Skip to content

Commit

Permalink
✨ RedocModule now accepts custom options and theme
Browse files Browse the repository at this point in the history
Bumped a few dependencies versions, added interfaces and joi for parameter validation, pretty much all of the options ReDoc offers are now implemented
  • Loading branch information
Alfonso Reyes committed Jul 11, 2019
1 parent 75a3328 commit cea94c7
Show file tree
Hide file tree
Showing 10 changed files with 434 additions and 77 deletions.
14 changes: 14 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false

[*.{yml,json,lock}]
indent_size = 2
74 changes: 72 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,72 @@
# nestjs-redoc
📘 ReDoc frontend for you NestJS swagger API documentation
<h1 align="center">
NestJS-Redoc
<h4 align="center">ReDoc powered frontend for your NestJS API spec</h4>
</h1>
<br />

<div align="center">
<a href="http://makeapullrequest.com">
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square" alt="PRs welcome" />
</a>
<a href="https://github.com/nestjs/nest">
<img src="https://raw.githubusercontent.com/nestjsx/crud/master/img/nest-powered.svg?sanitize=true" alt="Nest Powered" />
</a>
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-blue.svg"></a>
</div>

<p align="center">
<a href="#-features">Features</a> •
<a href="#-how-to-use">How to use</a> •
<a href="#-changelog">Changelog</a> •
</p>

<p align="center">
⚡ This is a ReDoc powered frontend for your NestJS API spec. By using ReDoc you improve your documentation presentation using a better UI/UX design
</p>

## ⚡ Features

TBD

## Installation

Using npm: ```npm i nestjs-redoc```

Using yarn: ```yarn add nestjs-redoc```

## ❓ How to use

You need to install the Swagger module first if you want to get definitions, otherwise you may use a URL parameter for an OpenAPI definition instead of document object.

```typescript
const options = new DocumentBuilder()
.setTitle('Look, i have a title')
.setDescription('A very nice description')
.setBasePath('/api/v1')
.build();
const document = SwaggerModule.createDocument(app, options);
```

Then add the followring example code.

**Note**: All properties are optional, if you don't specify a title we will fallback to the one you used above.

```typescript
const redocOptions: RedocOptions = {
title: 'Hello Nest',
logo: {
url: 'https://redocly.github.io/redoc/petstore-logo.png',
backgroundColor: '#F0F0F0',
altText: 'PetStore logo'
},
sortPropsAlphabetically: true,
hideDownloadButton: false,
hideHostname: false
};
// Instead of using SwaggerModule.setup() you call this module
RedocModule.setup('/docs', app, document, redocOptions);
```


## 📋 ToDo
bla bla
20 changes: 11 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,28 @@
"publish": "npm publish --access public"
},
"dependencies": {
"@hapi/joi": "^15.1.0",
"express-handlebars": "^3.1.0"
},
"devDependencies": {
"@nestjs/common": "^6.4.1",
"@nestjs/core": "^6.4.1",
"@nestjs/platform-express": "^6.4.1",
"@nestjs/common": "^6.5.2",
"@nestjs/core": "^6.5.2",
"@nestjs/platform-express": "^6.5.2",
"@nestjs/swagger": "^3.0.2",
"@types/express": "^4.17.0",
"@types/express-handlebars": "^0.0.32",
"@types/node": "^12.0.12",
"@types/hapi__joi": "^15.0.2",
"@types/node": "^12.6.2",
"reflect-metadata": "^0.1.13",
"rimraf": "^2.6.3",
"rxjs": "^6.5.2",
"typescript": "^3.5.2"
"tslint": "^5.18.0",
"typescript": "^3.5.3"
},
"peerDependencies": {
"@nestjs/common": "^6.0.0",
"@nestjs/core": "^6.0.0",
"@nestjs/common": "^6.5.2",
"@nestjs/core": "^6.5.2",
"@nestjs/swagger": "^3.0.2",
"express-handlebars": "^3.1.0",
"rxjs": "^6.5.2"
"express-handlebars": "^3.1.0"
}
}
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from './redoc-module';
export * from './interfaces';
export * from './interfaces';
1 change: 1 addition & 0 deletions src/interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './redoc_options.interface';
export * from './redoc_document.interface';
8 changes: 8 additions & 0 deletions src/interfaces/redoc_document.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { SwaggerDocument } from '@nestjs/swagger';
import { LogoOptions } from './redoc_options.interface';

export interface RedocDocument extends Partial<SwaggerDocument> {
info: SwaggerDocument['info'] & {
'x-logo'?: LogoOptions
};
}
49 changes: 47 additions & 2 deletions src/interfaces/redoc_options.interface.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,54 @@
export interface RedocOptions {
/** Web site title (e.g: ReDoc documentation) */
title?: string;
theme?: RedocTheme;
imgUrl?: string;
/** Logo Options */
logo?: LogoOptions;
/** Theme options */
theme?: any;
/** If set, the spec is considered untrusted and all HTML/markdown is sanitized to prevent XSS, by default is false */
untrustedSpec?: boolean;
/** If set, warnings are not rendered at the top of documentation (they are still logged to the console) */
supressWarnings?: boolean;
/** If set, the protocol and hostname won't be shown in the operation definition */
hideHostname?: boolean;
/** Specify which responses to expand by default by response codes,
* values should be passed as comma-separated list without spaces
* (e.g: 200, 201, "all")
*/
expandResponses?: string;
/** If set, show required properties first ordered in the same order as in required array */
requiredPropsFirst?: boolean;
/** If set, propeties will be sorted alphabetically */
sortPropsAlphabetically?: boolean;
/** If set the fields starting with "x-" will be showed, can be a boolean or a string with names of extensions to display */
showExtensions?: boolean | string;
/** If set, redoc won't inject authentication section automatically */
noAutoAuth?: boolean;
/** If set, path link and HTTP verb will be shown in the middle panel instead of the right one */
pathInMiddlePanel?: boolean;
/** If set, loading spinner animation won't be shown */
hideLoading?: boolean;
/** If set, a native scrollbar will be used instead of perfect-scroll, this can improve performance of the frontend for big specs */
nativeScrollbars?: boolean;
/** This will hide the "Download spec" button, it only hides the button */
hideDownloadButton?: boolean;
/** If set, the search bar will be disable */
disableSearch?: boolean;
/** SHows only required fileds in request samples */
onlyRequiredInSamples?: boolean;
}

export interface RedocTheme {
color: '';
}

export interface LogoOptions {
/** The URL pointing to the spec logo, must be in the format of a URL and an absolute URL */
url?: string;
/** Background color to be used, must be RGB color in hexadecimal format (e.g: #008080) */
backgroundColor?: string;
/** Alt tag for logo */
altText?: string;
/** href tag for logo, it defaults to the one used in your API spec */
href?: string;
}
110 changes: 96 additions & 14 deletions src/redoc-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,113 @@ import { NestExpressApplication } from '@nestjs/platform-express';
import { SwaggerDocument } from '@nestjs/swagger';
import { Request, Response } from 'express';
import { join } from 'path';
import { RedocOptions } from './interfaces';
import { LogoOptions, RedocDocument, RedocOptions } from './interfaces';
import Joi = require('@hapi/joi');
import exphbs = require('express-handlebars');

export class RedocModule {

public static setup(url: string, app: INestApplication, document: SwaggerDocument, options: RedocOptions) {
const httpAdapter: HttpServer = app.getHttpAdapter();
if (httpAdapter && httpAdapter.constructor && httpAdapter.constructor.name === 'FastifyAdapter') {
return this.setupFastify();
}
return this.setupExpress(url, <NestExpressApplication>app, document, options);
/**
* Setup ReDoc frontend
* @param path - path to mount the ReDoc frontend
* @param app - NestApplication
* @param document - Swagger document object
* @param options - Init options
*/
public static setup(path: string, app: INestApplication, document: SwaggerDocument, options: RedocOptions) {
// Validate options object
this.validateOptionsObject(options, document).then((_options) => {
const redocDocument = this.addVendorExtensions(_options, <RedocDocument>document);
const httpAdapter: HttpServer = app.getHttpAdapter();
if (httpAdapter && httpAdapter.constructor && httpAdapter.constructor.name === 'FastifyAdapter') {
return this.setupFastify();
}
return this.setupExpress(path, <NestExpressApplication>app, redocDocument, _options);
}, (err) => {
// oh ohh, something bad happened
throw new TypeError(err);
});
}

private static setupFastify() {
// throw new NotImplementedException('Fastify is not implemented yet');
}

private static setupExpress(url: string, app: NestExpressApplication, document: SwaggerDocument, options: RedocOptions) {
const finalPath = this.normalizePath(url);
private static validateOptionsObject(options: RedocOptions, document: SwaggerDocument): Joi.ValidationResult<RedocOptions> {
const schema = Joi.object().keys({
title: Joi.string().optional().default((document.info ? document.info.title : 'Swagger documentation')),
logo: {
url: Joi.string().optional().uri(),
backgroundColor: Joi.string().optional().regex(new RegExp('^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$')),
altText: Joi.string().optional(),
href: Joi.string().optional().uri()
},
theme: Joi.any().default(undefined),
untrustedSpec: Joi.boolean().optional().default(false),
supressWarnings: Joi.boolean().optional().default(true),
hideHostname: Joi.boolean().optional().default(false),
expandResponses: Joi.string().optional(),
requiredPropsFirst: Joi.boolean().optional().default(true),
sortPropsAlphabetically: Joi.boolean().optional().default(true),
showExtensions: Joi.any().optional().default(false),
noAutoAuth: Joi.boolean().optional().default(true),
pathInMiddlePanel: Joi.boolean().optional().default(false),
hideLoading: Joi.boolean().optional().default(false),
nativeScrollbars: Joi.boolean().optional().default(false),
hideDownloadButton: Joi.boolean().optional().default(false),
disableSearch: Joi.boolean().optional().default(false),
onlyRequiredInSamples: Joi.boolean().optional().default(false)
});
return schema.validate(options);
}

/**
* Setup ReDoc frontend for express plattform
* @param path - path to mount the ReDoc frontend
* @param app - NestApplication
* @param document - ReDoc document object
* @param options - Init options
*/
private static setupExpress(path: string, app: NestExpressApplication, document: RedocDocument, options: RedocOptions) {
// Normalize URL path to use
const finalPath = this.normalizePath(path);
// Serve swagger spec in another URL appended to the normalized path
const swaggerDocUrl = join(finalPath, 'swagger.json');
const { title } = options;
app.engine('handlebars', exphbs());
const hbs = exphbs.create({
helpers: {
toJSON: function (object: any) {
return JSON.stringify(object);
}
}
});
// Set app to use handlebars as engine
app.engine('handlebars', hbs.engine);
app.set('view engine', 'handlebars');
// Set views folder
app.set('views', join(__dirname, '..', 'views'));
// Serve ReDoc Frontend
app.getHttpAdapter().get(finalPath, (req: Request, res: Response) => {
res.render('redoc', {
layout: false,
const { title, theme, logo, ...otherOptions } = options;
const renderData = {
data: {
title: title,
url: swaggerDocUrl
docUrl: swaggerDocUrl,
options: otherOptions,
...(theme && {
theme: {
...theme
}
})
}
};
res.render('redoc', {
/** Tell handlebars to not use a main layout */
layout: false,
debug: renderData,
...renderData
});
});
// Serve swagger spec json
app.getHttpAdapter().get(swaggerDocUrl, (req: Request, res: Response) => {
res.setHeader('Content-Type', 'application/json');
res.send(document);
Expand All @@ -45,4 +119,12 @@ export class RedocModule {
private static normalizePath(path: string) {
return path.charAt(0) !== '/' ? '/' + path : path;
}

private static addVendorExtensions(options: RedocOptions, document: RedocDocument) {
if (options.logo) {
const logoOption: Partial<LogoOptions> = { ...options.logo };
document.info['x-logo'] = logoOption;
}
return document;
}
}
38 changes: 10 additions & 28 deletions views/redoc.handlebars
Original file line number Diff line number Diff line change
Expand Up @@ -19,40 +19,22 @@
</style>
</head>

<body>
<body>
<!-- we provide is specification here -->
<div id="redoc_container"></div>
<script src="https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js"> </script>
<script>
let themeJSON = '{{{ toJSON data.theme }}}';
if (themeJSON === '') { themeJSON = undefined }
Redoc.init(
'{{ data.url }}',
'{{ data.docUrl }}',
{
"showConsole": true,
"lazyRendering": true,
"theme": {
"logo": { "gutter": "20px 30px 14px 30px" },
"colors": {
"primary": {
"main": "#000"
},
"text": { "primary": "#000", "secondary": "#008080" }
},
"typography": {
"headings": { "fontFamily": "Roboto" },
"fontFamily": "Roboto, Verdana, Geneva, sans-serif",
"fontSize": "16px",
"code": { "fontFamily": "\"Courier New\",monospace", "tokens": { "token.property": { "color": "#aofbaa" }, "string": { "color": "#aofbaa" } } },
},
"rightPanel": { "backgroundColor": "#212121", "width": "50%" },
"menu": {
"backgroundColor": "#fff",
"arrow": { "size": '2em' },
"groupItems": {
"textTransform": 'capitalize'
}
},
"links": { "color": "#6CC496" }
}
...(themeJSON && {
theme: {
...JSON.parse(themeJSON)
}
}),
...JSON.parse('{{{ toJSON data.options }}}')
},
document.getElementById("redoc_container")
);
Expand Down
Loading

0 comments on commit cea94c7

Please sign in to comment.