Merge commit 'cfb6f0fde0d5b3bc57f8eb6c7cb90e6f39017116'

# Conflicts:
#	src/common-elements/panels.ts
#	src/components/ApiLogo/ApiLogo.tsx
This commit is contained in:
Eduardo Perez 2018-12-11 14:02:40 -05:00
commit ce84bafad5
57 changed files with 5100 additions and 2937 deletions

12
.dockerignore Normal file
View File

@ -0,0 +1,12 @@
*
!src/
!config
!demo/favicon.png
!custom.d.ts
!typings/styled-patch.d.ts
!tsconfig.json
!webpack.config.ts
!package.json
!yarn.lock

View File

@ -27,7 +27,7 @@ You will need [Node.js](http://nodejs.org) at `v8.0.0+` and [Yarn](https://yarnp
After cloning the repo, run:
```bash
$ yarn install # or yarn
$ yarn install # or npm
```
### Commonly used NPM scripts
@ -91,4 +91,4 @@ There are some other scripts available in the `scripts` section of the `package.
- **`src/types`**: contains extra typescript typings including OpenAPI doc typings
- **`src/utils`**: utility functions
- **`src/styled-components.ts`**: - reexprots styled-components with proper typescript annotations using theme
- **`src/theme.ts`**: - default theme (colors, fonts, etc) used by all the components
- **`src/theme.ts`**: - default theme (colors, fonts, etc) used by all the components

View File

@ -1,3 +1,42 @@
# [2.0.0-rc.0](https://github.com/Rebilly/ReDoc/compare/v2.0.0-alpha.41...v2.0.0-rc.0) (2018-11-27)
### Bug Fixes
* false-positive recursive detection with oneOf ([59eaa8d](https://github.com/Rebilly/ReDoc/commit/59eaa8d)), closes [#723](https://github.com/Rebilly/ReDoc/issues/723) [#585](https://github.com/Rebilly/ReDoc/issues/585)
* fix hideHostname also hiding basePath ([b5f3224](https://github.com/Rebilly/ReDoc/commit/b5f3224)), closes [#677](https://github.com/Rebilly/ReDoc/issues/677)
* fix spacing with nested markdown lists ([f2f6909](https://github.com/Rebilly/ReDoc/commit/f2f6909)), closes [#718](https://github.com/Rebilly/ReDoc/issues/718)
* improve scrolling performance in Chrome with non-wrapped json examples ([a69c402](https://github.com/Rebilly/ReDoc/commit/a69c402))
* nested oneOf button spacing ([3673720](https://github.com/Rebilly/ReDoc/commit/3673720)), closes [#719](https://github.com/Rebilly/ReDoc/issues/719)
* onLoaded callback not run on spec error ([e77df0c](https://github.com/Rebilly/ReDoc/commit/e77df0c)), closes [#690](https://github.com/Rebilly/ReDoc/issues/690)
* theme improvments by [@stasiukanya](https://github.com/stasiukanya) ([e2d0cd5](https://github.com/Rebilly/ReDoc/commit/e2d0cd5))
* **cli:** old peer dependency issue with styled-components ([#699](https://github.com/Rebilly/ReDoc/issues/699)) ([9e2853c](https://github.com/Rebilly/ReDoc/commit/9e2853c))
### Features
* Add feature to specify href for logo explicitly ([#645](https://github.com/Rebilly/ReDoc/issues/645)) ([87fd7d7](https://github.com/Rebilly/ReDoc/commit/87fd7d7))
* add support for markdown in Server Object ([155d214](https://github.com/Rebilly/ReDoc/commit/155d214))
* Add support for minLength and maxLength constraint humanization ([#700](https://github.com/Rebilly/ReDoc/issues/700)) ([f40568b](https://github.com/Rebilly/ReDoc/commit/f40568b)), closes [#42](https://github.com/Rebilly/ReDoc/issues/42) [/github.com/Rebilly/ReDoc/issues/42#issuecomment-371883853](https://github.com//github.com/Rebilly/ReDoc/issues/42/issues/issuecomment-371883853)
<a name="2.0.0-alpha.41"></a>
# [2.0.0-alpha.41](https://github.com/Rebilly/ReDoc/compare/v2.0.0-alpha.40...v2.0.0-alpha.41) (2018-10-18)
### Bug Fixes
* add null check in dispose method ([#675](https://github.com/Rebilly/ReDoc/issues/675)) ([6b7c5b7](https://github.com/Rebilly/ReDoc/commit/6b7c5b7))
* extensionHook not being used ([a4a4013](https://github.com/Rebilly/ReDoc/commit/a4a4013)), closes [#665](https://github.com/Rebilly/ReDoc/issues/665)
* fix issue with broken markdown caused by marked bug ([70cf293](https://github.com/Rebilly/ReDoc/commit/70cf293))
### Peer dependencies updates
* ReDoc now requires `styled-components@^4.0.1` to be installed if used as React component
<a name="2.0.0-alpha.40"></a>
# [2.0.0-alpha.40](https://github.com/Rebilly/ReDoc/compare/v2.0.0-alpha.39...v2.0.0-alpha.40) (2018-10-05)

View File

@ -5,7 +5,7 @@
[![Build Status](https://travis-ci.org/Rebilly/ReDoc.svg?branch=master)](https://travis-ci.org/Rebilly/ReDoc) [![Coverage Status](https://coveralls.io/repos/Rebilly/ReDoc/badge.svg?branch=master&service=github)](https://coveralls.io/github/Rebilly/ReDoc?branch=master) [![dependencies Status](https://david-dm.org/Rebilly/ReDoc/status.svg)](https://david-dm.org/Rebilly/ReDoc) [![devDependencies Status](https://david-dm.org/Rebilly/ReDoc/dev-status.svg)](https://david-dm.org/Rebilly/ReDoc#info=devDependencies) [![npm](http://img.shields.io/npm/v/redoc.svg)](https://www.npmjs.com/package/redoc) [![License](https://img.shields.io/npm/l/redoc.svg)](https://github.com/Rebilly/ReDoc/blob/master/LICENSE)
[![bundle size](http://img.badgesize.io/https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js?compression=gzip&max=300000)](https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js) [![npm](https://img.shields.io/npm/dm/redoc.svg)](https://www.npmjs.com/package/redoc) [![](https://data.jsdelivr.com/v1/package/npm/redoc/badge)](https://www.jsdelivr.com/package/npm/redoc)
[![bundle size](http://img.badgesize.io/https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js?compression=gzip&max=300000)](https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js) [![npm](https://img.shields.io/npm/dm/redoc.svg)](https://www.npmjs.com/package/redoc) [![](https://data.jsdelivr.com/v1/package/npm/redoc/badge)](https://www.jsdelivr.com/package/npm/redoc) [![Docker Build Status](https://img.shields.io/docker/build/redocly/redoc.svg)](https://hub.docker.com/r/redocly/redoc/)
</div>
@ -184,6 +184,17 @@ You can also specify `onLoaded` callback which will be called each time Redoc ha
/>
```
## The Docker way
ReDoc is available as pre-built Docker image in official [Docker Hub repository](https://hub.docker.com/r/redocly/redoc/). You may simply pull & run it:
docker pull redocly/redoc
docker run -p 8080:80 redocly/redoc
Also you may rewrite some predefined environment variables defined in [Dockerfile](./config/docker/Dockerfile). By default ReDoc starts with demo Petstore spec located at `http://petstore.swagger.io/v2/swagger.json`, but you may change this URL using environment variable `SPEC_URL`:
docker run -p 8080:80 -e SPEC_URL=https://api.example.com/openapi.json redocly/redoc
## ReDoc CLI
[See here](https://github.com/Rebilly/ReDoc/blob/master/cli/README.md)
@ -194,7 +205,7 @@ You can also specify `onLoaded` callback which will be called each time Redoc ha
You can inject Security Definitions widget into any place of your specification `description`. Check out details [here](docs/security-definitions-injection.md).
### Swagger vendor extensions
ReDoc makes use of the following [vendor extensions](http://swagger.io/specification/#vendorExtensions):
ReDoc makes use of the following [vendor extensions](https://swagger.io/specification/#specificationExtensions):
* [`x-logo`](docs/redoc-vendor-extensions.md#x-logo) - is used to specify API logo
* [`x-traitTag`](docs/redoc-vendor-extensions.md#x-traitTag) - useful for handling out common things like Pagination, Rate-Limits, etc
* [`x-code-samples`](docs/redoc-vendor-extensions.md#x-code-samples) - specify operation code samples

23
cli/Dockerfile Normal file
View File

@ -0,0 +1,23 @@
# Package the 'redoc-cli' as a docker image.
#
# To build:
# $ cd <Redoc project directory>
# $ docker build -t redoc-cli -f cli/Dockerfile .
#
# To run:
# To display the command line options:
# $ docker run --rm -it redoc-cli --help
# .. will display the comand line help
#
# To turn `swagger.yml` file in the current directory, to html documentation 'redoc-static.html'
# $ docker run --rm -it -v $PWD:/data redoc-cli bundle swagger.yml
FROM node:alpine
RUN npm install -g redoc-cli
WORKDIR /data
EXPOSE 8080
ENTRYPOINT ["redoc-cli"]
CMD []

View File

@ -5,15 +5,16 @@ import { renderToString } from 'react-dom/server';
import { ServerStyleSheet } from 'styled-components';
import { compile } from 'handlebars';
import { createServer, ServerRequest, ServerResponse } from 'http';
import { dirname, join } from 'path';
import { createServer, IncomingMessage, ServerResponse } from 'http';
import { dirname, join, resolve } from 'path';
import * as zlib from 'zlib';
// @ts-ignore
import { createStore, loadAndBundleSpec, Redoc } from 'redoc';
import { createReadStream, existsSync, readFileSync, ReadStream, watch, writeFileSync } from 'fs';
import {watch} from 'chokidar';
import { createReadStream, existsSync, readFileSync, ReadStream, writeFileSync } from 'fs';
import * as mkdirp from 'mkdirp';
import * as YargsParser from 'yargs';
@ -167,28 +168,25 @@ async function serve(port: number, pathToSpec: string, options: Options = {}) {
server.listen(port, () => console.log(`Server started: http://127.0.0.1:${port}`));
if (options.watch && existsSync(pathToSpec)) {
const pathToSpecDirectory = dirname(pathToSpec);
const pathToSpecDirectory = resolve(dirname(pathToSpec));
const watchOptions = {
recursive: true,
ignored: /(^|[\/\\])\../,
};
watch(
pathToSpecDirectory,
watchOptions,
debounce(async (event, filename) => {
if (event === 'change' || event === 'rename') {
console.log(`${join(pathToSpecDirectory, filename)} 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 ${pathToSpecDirectory} for changes...`);
const watcher = watch(pathToSpecDirectory, watchOptions);
const log = console.log.bind(console);
watcher
.on('change', async path => {
log(`${path} changed, updating docs`);
try {
spec = await loadAndBundleSpec(pathToSpec);
pageHTML = await getPageHTML(spec, pathToSpec, options);
log('Updated successfully');
} catch (e) {
console.error('Error while updating: ', e.message);
}})
.on('error', error => console.error(`Watcher error: ${error}`))
.on('ready', () => log(`👀 Watching ${pathToSpecDirectory} for changes...`));
}
}
@ -258,7 +256,7 @@ async function getPageHTML(
// credits: https://stackoverflow.com/a/9238214/1749888
function respondWithGzip(
contents: string | ReadStream,
request: ServerRequest,
request: IncomingMessage,
response: ServerResponse,
headers = {},
) {
@ -291,17 +289,6 @@ function respondWithGzip(
}
}
function debounce(callback: (...args) => void, time: number) {
let interval;
return (...args) => {
clearTimeout(interval);
interval = setTimeout(() => {
interval = null;
callback(...args);
}, time);
};
}
function isURL(str: string): boolean {
return /^(https?:)\/\//m.test(str);
}

View File

@ -1,6 +1,6 @@
{
"name": "redoc-cli",
"version": "0.6.4",
"version": "0.7.0",
"description": "ReDoc's Command Line Interface",
"main": "index.js",
"bin": "index.js",
@ -8,16 +8,17 @@
"author": "Roman Hotsiy <gotsijroman@gmail.com>",
"license": "MIT",
"dependencies": {
"chokidar": "^2.0.4",
"handlebars": "^4.0.11",
"isarray": "^2.0.4",
"mkdirp": "^0.5.1",
"mobx": "^4.2.0",
"react": "^16.4.2",
"react-dom": "^16.4.2",
"redoc": "^2.0.0-alpha.37",
"styled-components": "^3.4.0",
"react": "^16.6.3",
"react-dom": "^16.6.3",
"redoc": "^2.0.0-alpha.41",
"styled-components": "^4.1.1",
"tslib": "^1.9.3",
"yargs": "^12.0.1"
"yargs": "^12.0.5"
},
"scripts": {
"ci-publish": "ci-publish"
@ -26,6 +27,7 @@
"access": "public"
},
"devDependencies": {
"@types/chokidar": "^1.7.5",
"@types/handlebars": "^4.0.39",
"@types/mkdirp": "^0.5.2",
"ci-publish": "^1.3.1"

File diff suppressed because it is too large Load Diff

View File

@ -9,10 +9,16 @@ FROM node:alpine
RUN apk update && apk add --no-cache git
# generate bundle
# Install dependencies
WORKDIR /build
COPY . /build
COPY package.json yarn.lock /build/
RUN yarn install --frozen-lockfile --ignore-optional --ignore-scripts
# copy only required for the build files
COPY src /build/src
COPY webpack.config.ts tsconfig.json custom.d.ts /build/
COPY typings/styled-patch.d.ts /build/typings/styled-patch.d.ts
RUN npm run bundle:standalone
FROM nginx:alpine

View File

@ -1,8 +1,4 @@
# Redoc docker image
## Build
docker build -t redoc .
# Official ReDoc Docker Image
## Usage
@ -23,4 +19,8 @@ Serve local file:
- `PAGE_FAVICON` (default `"favicon.png"`) - URL to page favicon
- `SPEC_URL` (default `"http://petstore.swagger.io/v2/swagger.json"`) - URL to spec
- `PORT` (default `80`) - nginx port
- `REDOC_OPTIONS` - [`<redoc>` tag attributes](https://github.com/Rebilly/ReDoc#redoc-tag-attributes)
- `REDOC_OPTIONS` - [`<redoc>` tag attributes](https://github.com/Rebilly/ReDoc#redoc-tag-attributes)
## Build
docker build -t redoc .

6
config/docker/hooks/build Executable file
View File

@ -0,0 +1,6 @@
#!/bin/bash
# DockerHub cd into Dockerfile location before buil
# So we have to undo this.
cd ../..
docker build -f config/docker/Dockerfile -t $IMAGE_NAME .

View File

@ -20,7 +20,7 @@
<body>
<redoc spec-url="%SPEC_URL%" %REDOC_OPTIONS%></redoc>
<script src="/redoc.standalone.js"></script>
<script src="redoc.standalone.js"></script>
</body>
</html>

View File

@ -4,16 +4,10 @@
*/
import * as React from 'react';
import styled, { StyledFunction } from 'styled-components';
import styled from '../src/styled-components';
function withProps<T, U extends HTMLElement = HTMLElement>(
styledFunction: StyledFunction<React.HTMLProps<U>>,
): StyledFunction<T & React.HTMLProps<U>> {
return styledFunction;
}
const DropDownItem = withProps<{ active: boolean }>(styled.li)`
${props => ((props as any).active ? 'background-color: #eee' : '')};
const DropDownItem = styled.li<{ active?: boolean }>`
${(props: any) => (props.active ? 'background-color: #eee' : '')};
padding: 13px 16px;
&:hover {
background-color: #eee;

View File

@ -296,6 +296,8 @@ paths:
style: form
schema:
type: array
minItems: 1
maxItems: 3
items:
type: string
enum:
@ -784,6 +786,7 @@ components:
photoUrls:
description: The list of URL to a cute photos featuring pet
type: array
maxItems: 20
xml:
name: photoUrl
wrapped: true
@ -796,6 +799,7 @@ components:
tags:
description: Tags attached to the pet
type: array
minItems: 1
xml:
name: tag
wrapped: true

View File

@ -36,6 +36,7 @@ const babelLoader = mode => ({
plugins: compact([
['@babel/plugin-syntax-typescript', { isTSX: true }],
['@babel/plugin-syntax-decorators', { legacy: true }],
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-syntax-jsx',
mode !== 'production' ? 'react-hot-loader/babel' : undefined,
[
@ -96,7 +97,7 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
{
test: /\.tsx?$/,
use: [tsLoader(env), babelLoader(mode)],
exclude: ['node_modules'],
exclude: [/node_modules/],
},
{
test: /\.css$/,
@ -132,7 +133,11 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
new webpack.NamedModulesPlugin(),
new webpack.optimize.ModuleConcatenationPlugin(),
new HtmlWebpackPlugin({
template: env.playground ? 'demo/playground/index.html' : 'demo/index.html',
template: env.playground
? 'demo/playground/index.html'
: env.bench
? 'benchmark/index.html'
: 'demo/index.html',
}),
new ForkTsCheckerWebpackPlugin(),
ignore(/js-yaml\/dumper\.js$/),

View File

@ -1,10 +1,10 @@
# ReDoc vendor extensions
ReDoc makes use of the following [vendor extensions](http://swagger.io/specification/#vendorExtensions)
ReDoc makes use of the following [vendor extensions](https://swagger.io/specification/#specificationExtensions)
### Swagger Object vendor extensions
Extend OpenAPI root [Swagger Object](http://swagger.io/specification/#swaggerObject)
Extend OpenAPI root [Swagger Object](https://swagger.io/specification/#oasObject)
#### x-servers
Backported from OpenAPI 3.0 [`servers`](https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.md#server-object). Currently doesn't support templates.
Backported from OpenAPI 3.0 [`servers`](https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.0.md#server-object). Currently doesn't support templates.
#### x-tagGroups
@ -96,6 +96,7 @@ The information about API logo
| url | string | The URL pointing to the spec logo. MUST be in the format of a URL. It SHOULD be an absolute URL so your API definition is usable from any location
| backgroundColor | string | background color to be used. MUST be RGB color in [hexadecimal format] (https://en.wikipedia.org/wiki/Web_colors#Hex_triplet)
| altText | string | Text to use for alt tag on the logo. Defaults to 'logo' if nothing is provided.
| href | string | The URL pointing to the contact page. Default to 'info.contact.url' field of the OAS.
###### x-logo example
@ -205,7 +206,7 @@ Extends OpenAPI [Parameter Object](http://swagger.io/specification/#parameterObj
`x-examples` are rendered in the JSON tab on the right panel of ReDoc.
### Response Object vendor extensions
Extends OpenAPI [Response Object](https://swagger.io/specification/#responseObject)
Extends OpenAPI [Response Object](https://swagger.io/specification/#requestBodyObject)
#### x-summary
| Field Name | Type | Description |

View File

@ -1,6 +1,6 @@
{
"name": "redoc",
"version": "2.0.0-alpha.40",
"version": "2.0.0-rc.0",
"description": "ReDoc",
"repository": {
"type": "git",
@ -48,84 +48,87 @@
"compile:cli": "tsc custom.d.ts cli/index.ts --target es6 --module commonjs --types yargs",
"build:demo": "webpack --mode=production --config demo/webpack.config.ts",
"deploy:demo": "npm run build:demo && deploy-to-gh-pages --update demo/dist",
"license-check": "license-checker --production --onlyAllow 'MIT;ISC;Apache-2.0;BSD;BSD-2-Clause;BSD-3-Clause' --summary"
"license-check": "license-checker --production --onlyAllow 'MIT;ISC;Apache-2.0;BSD;BSD-2-Clause;BSD-3-Clause' --summary",
"docker:build": "docker build -f config/docker/Dockerfile -t redoc ."
},
"devDependencies": {
"@babel/core": "7.0.0-rc.2",
"@babel/plugin-syntax-decorators": "7.0.0-rc.2",
"@babel/plugin-syntax-jsx": "7.0.0-rc.2",
"@babel/plugin-syntax-typescript": "7.0.0-rc.2",
"@cypress/webpack-preprocessor": "2.0.1",
"@types/chai": "4.1.4",
"@types/dompurify": "^0.0.31",
"@types/enzyme": "^3.1.13",
"@babel/core": "7.1.6",
"@babel/plugin-syntax-decorators": "7.1.0",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/plugin-syntax-jsx": "7.0.0",
"@babel/plugin-syntax-typescript": "7.1.5",
"@cypress/webpack-preprocessor": "4.0.2",
"@types/chai": "4.1.7",
"@types/dompurify": "^0.0.32",
"@types/enzyme": "^3.1.15",
"@types/enzyme-to-json": "^1.5.2",
"@types/jest": "^23.3.1",
"@types/jest": "^23.3.9",
"@types/json-pointer": "^1.0.30",
"@types/lodash": "^4.14.116",
"@types/lodash": "^4.14.118",
"@types/lunr": "^2.1.6",
"@types/mark.js": "^8.11.1",
"@types/marked": "^0.4.1",
"@types/marked": "^0.4.2",
"@types/prismjs": "^1.6.4",
"@types/prop-types": "^15.5.5",
"@types/react": "^16.4.11",
"@types/react-dom": "^16.0.7",
"@types/prop-types": "^15.5.6",
"@types/react": "^16.7.7",
"@types/react-dom": "^16.0.10",
"@types/react-hot-loader": "^4.1.0",
"@types/react-tabs": "^1.0.5",
"@types/react-tabs": "^2.3.0",
"@types/styled-components": "^4.1.1",
"@types/tapable": "1.0.4",
"@types/webpack": "^4.4.11",
"@types/webpack": "^4.4.19",
"@types/webpack-env": "^1.13.0",
"@types/yargs": "^11.1.1",
"babel-loader": "8.0.0-beta.2",
"babel-plugin-styled-components": "^1.5.1",
"@types/yargs": "^12.0.1",
"babel-loader": "8.0.4",
"babel-plugin-styled-components": "^1.9.0",
"beautify-benchmark": "^0.2.4",
"bundlesize": "^0.17.0",
"conventional-changelog-cli": "^2.0.5",
"copy-webpack-plugin": "^4.5.2",
"conventional-changelog-cli": "^2.0.11",
"copy-webpack-plugin": "^4.6.0",
"core-js": "^2.5.7",
"coveralls": "^3.0.2",
"css-loader": "^1.0.0",
"cypress": "~3.1.0",
"css-loader": "^1.0.1",
"cypress": "~3.1.2",
"deploy-to-gh-pages": "^1.3.6",
"enzyme": "^3.4.4",
"enzyme-adapter-react-16": "^1.2.0",
"enzyme": "^3.7.0",
"enzyme-adapter-react-16": "^1.7.0",
"enzyme-to-json": "^3.3.4",
"fork-ts-checker-webpack-plugin": "0.4.3",
"fork-ts-checker-webpack-plugin": "0.5.0",
"html-webpack-plugin": "^3.1.0",
"jest": "^23.5.0",
"license-checker": "^20.2.0",
"lodash": "^4.17.10",
"jest": "^23.6.0",
"license-checker": "^24.0.1",
"lodash": "^4.17.11",
"mobx": "^4.3.1",
"prettier": "^1.14.2",
"prettier": "^1.15.2",
"prettier-eslint": "^8.8.2",
"puppeteer": "^1.7.0",
"raf": "^3.4.0",
"react": "^16.4.2",
"react-dom": "^16.4.2",
"puppeteer": "^1.10.0",
"raf": "^3.4.1",
"react": "^16.6.3",
"react-dom": "^16.6.3",
"rimraf": "^2.6.2",
"shelljs": "^0.8.1",
"shelljs": "^0.8.3",
"source-map-loader": "^0.2.4",
"style-loader": "^0.22.1",
"styled-components": "^3.4.5",
"swagger2openapi": "^3.2.8",
"ts-jest": "23.0.1",
"ts-loader": "4.5.0",
"style-loader": "^0.23.1",
"styled-components": "^4.1.1",
"swagger2openapi": "^3.2.14",
"ts-jest": "23.10.5",
"ts-loader": "5.3.1",
"ts-node": "^7.0.1",
"tslint": "^5.11.0",
"tslint-react": "^3.4.0",
"typescript": "^3.0.1",
"webpack": "^4.17.1",
"webpack-cli": "^3.1.0",
"webpack-dev-server": "^3.1.5",
"typescript": "^3.1.6",
"webpack": "^4.26.1",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.10",
"webpack-node-externals": "^1.6.0",
"workerize-loader": "^1.0.3",
"workerize-loader": "^1.0.4",
"yaml-js": "^0.2.3"
},
"peerDependencies": {
"mobx": "^4.2.0",
"react": "^16.2.0",
"react-dom": "^16.2.0",
"styled-components": "^3.4.0"
"styled-components": "^4.0.1"
},
"dependencies": {
"classnames": "^2.2.6",
@ -133,10 +136,10 @@
"dompurify": "^1.0.7",
"eventemitter3": "^3.0.0",
"json-pointer": "^0.6.0",
"json-schema-ref-parser": "^5.1.2",
"json-schema-ref-parser": "^6.0.1",
"lunr": "^2.3.2",
"mark.js": "^8.11.1",
"marked": "^0.5.1",
"marked": "^0.5.2",
"memoize-one": "^4.0.0",
"mobx-react": "^5.2.5",
"openapi-sampler": "1.0.0-beta.14",
@ -151,10 +154,6 @@
"stickyfill": "^1.1.1",
"tslib": "^1.9.3"
},
"resolutions": {
"@types/chai": "4.0.8",
"@types/tapable": "1.0.0"
},
"bundlesize": [
{
"path": "./bundles/redoc.standalone.js",
@ -162,30 +161,8 @@
}
],
"jest": {
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
"setupTestFrameworkScriptFile": "<rootDir>/src/setupTests.ts",
"testPathIgnorePatterns": [
"/node_modules/",
"/benchmark/"
],
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json"
],
"moduleNameMapper": {
"\\.(css|less)$": "<rootDir>/src/empty.js"
},
"globals": {
"ts-jest": {
"skipBabel": true
}
},
"preset": "ts-jest",
"collectCoverageFrom": [
"src/**/*.{ts,tsx}"
],
@ -195,8 +172,16 @@
"text-summary"
],
"coveragePathIgnorePatterns": [
"\\.d\\.ts$"
]
"\\.d\\.ts$",
"/benchmark/",
"/node_modules/"
],
"modulePathIgnorePatterns": [
"/benchmark/"
],
"moduleNameMapper": {
"\\.(css|less)$": "<rootDir>/src/empty.js"
}
},
"prettier": {
"singleQuote": true,

View File

@ -19,6 +19,7 @@ const Tip = styled.div`
border-radius: 4px;
padding: 0.3em 0.6em;
text-align: center;
box-shadow: 0px 0px 5px 0px rgba(204, 204, 204, 1);
`;
const Content = styled.div`

View File

@ -1,7 +1,6 @@
import Dropdown from 'react-dropdown';
import { StyledComponentClass } from 'styled-components';
import styled, { withProps } from '../styled-components';
import styled from '../styled-components';
export interface DropdownOption {
label: string;
@ -14,17 +13,17 @@ export interface DropdownProps {
onChange: (val: DropdownOption) => void;
}
export const StyledDropdown = withProps<DropdownProps>(styled(Dropdown))`
export const StyledDropdown = styled(Dropdown)`
min-width: 100px;
display: inline-block;
position: relative;
width: auto;
font-family: ${props => props.theme.typography.headings.fontFamily};
.Dropdown-control {
.Dropdown-control {
font-family: ${props => props.theme.typography.headings.fontFamily};
position: relative;
font-size: .929em;
font-size: 0.929em;
width: 100%;
line-height: 1.5em;
vertical-align: middle;
@ -39,6 +38,8 @@ export const StyledDropdown = withProps<DropdownProps>(styled(Dropdown))`
margin-top: 5px;
background: white;
box-sizing: border-box;
&:hover {
border-color: ${props => props.theme.colors.primary.main};
color: ${props => props.theme.colors.primary.main};
@ -84,14 +85,14 @@ export const StyledDropdown = withProps<DropdownProps>(styled(Dropdown))`
padding: 0.4em;
&.is-selected {
background-color: rgba(0, 0, 0, 0.05)
background-color: rgba(0, 0, 0, 0.05);
}
&:hover {
background-color: rgba(38, 50, 56, 0.12)
background-color: rgba(38, 50, 56, 0.12);
}
}
` as StyledComponentClass<any, DropdownProps>;
`;
export const SimpleDropdown = styled(StyledDropdown)`
margin-left: 10px;
@ -105,7 +106,7 @@ export const SimpleDropdown = styled(StyledDropdown)`
background: transparent;
&:hover {
color: ${props => props.theme.colors.main};
color: ${props => props.theme.colors.primary.main};
box-shadow: none;
}
}

View File

@ -1,6 +1,6 @@
// import { transparentize } from 'polished';
import styled, { extensionsHook, withProps } from '../styled-components';
import styled, { extensionsHook } from '../styled-components';
import { deprecatedCss } from './mixins';
export const PropertiesTableCaption = styled.caption`
@ -10,7 +10,7 @@ export const PropertiesTableCaption = styled.caption`
color: ${props => props.theme.colors.text.secondary};
`;
export const PropertyCell = styled.td`
export const PropertyCell = styled.td<{ kind?: string }>`
border-left: 1px solid ${props => props.theme.schema.linesColor};
box-sizing: border-box;
position: relative;
@ -58,7 +58,7 @@ export const PropertyCellWithInner = styled(PropertyCell)`
padding: 0;
`;
export const PropertyNameCell = withProps<{ kind?: string }>(styled(PropertyCell))`
export const PropertyNameCell = styled(PropertyCell)`
vertical-align: top;
line-height: 20px;
white-space: nowrap;

View File

@ -8,7 +8,7 @@ const headerFontSize = {
export const headerCommonMixin = level => css`
font-family: ${props => props.theme.typography.headings.fontFamily};
font-weight: 400;
font-weight: ${({ theme }) => theme.typography.headings.fontWeight};
font-size: ${headerFontSize[level]};
`;

View File

@ -1,5 +1,5 @@
import { SECTION_ATTR } from '../services/MenuStore';
import styled, { media, withProps } from '../styled-components';
import styled, { media } from '../styled-components';
export const MiddlePanel = styled.div`
width: calc(100% - ${props => props.theme.rightPanel.width});
@ -12,18 +12,16 @@ export const MiddlePanel = styled.div`
`};
`;
export const Section = withProps<{ underlined?: boolean }>(
styled.div.attrs({
[SECTION_ATTR]: props => props.id,
className: 'section',
} as any),
)`
export const Section = styled.div.attrs(props => ({
[SECTION_ATTR]: props.id,
className: props.className ? `section ${props.className}` : 'section',
})) <{ underlined?: boolean }>`
padding: ${props => props.theme.spacing.sectionVertical}px 0;
${media.lessThan('medium', true)`
padding: 0;
`}
${props =>
${(props: any) =>
(props.underlined &&
`
position: relative;
@ -42,7 +40,7 @@ export const Section = withProps<{ underlined?: boolean }>(
export const RightPanel = styled.div`
width: ${props => props.theme.rightPanel.width};
color: #fafbfc;
color: ${({ theme }) => theme.rightPanel.textColor};
background-color: ${props => props.theme.rightPanel.backgroundColor};
padding: 0 ${props => props.theme.spacing.sectionHorizontal}px;

View File

@ -4,7 +4,7 @@ import PerfectScrollbarType, * as PerfectScrollbarNamespace from 'perfect-scroll
import psStyles from 'perfect-scrollbar/css/perfect-scrollbar.css';
import { OptionsContext } from '../components/OptionsProvider';
import styled, { injectGlobal } from '../styled-components';
import styled, { createGlobalStyle } from '../styled-components';
/*
* perfect scrollbar umd bundle uses exports assignment while module uses default export
@ -14,7 +14,7 @@ import styled, { injectGlobal } from '../styled-components';
const PerfectScrollbarConstructor =
PerfectScrollbarNamespace.default || ((PerfectScrollbarNamespace as any) as PerfectScrollbarType);
injectGlobal`${psStyles && psStyles.toString()}`;
const PSStyling = createGlobalStyle`${psStyles && psStyles.toString()}`;
const StyledScrollWrapper = styled.div`
position: relative;
@ -58,9 +58,12 @@ export class PerfectScrollbar extends React.Component<PerfectScrollbarProps> {
}
return (
<StyledScrollWrapper className={`scrollbar-container ${className}`} innerRef={this.handleRef}>
{children}
</StyledScrollWrapper>
<>
<PSStyling />
<StyledScrollWrapper className={`scrollbar-container ${className}`} ref={this.handleRef}>
{children}
</StyledScrollWrapper>
</>
);
}
}

View File

@ -1,7 +1,7 @@
import styled, { withProps } from '../styled-components';
import styled from '../styled-components';
export const OneOfList = styled.ul`
margin: 0;
margin: 0 0 3px 0;
padding: 0;
list-style: none;
display: inline-block;
@ -15,7 +15,7 @@ export const OneOfLabel = styled.span`
}
`;
export const OneOfButton = withProps<{ active: boolean }>(styled.li)`
export const OneOfButton = styled.li<{ active: boolean }>`
display: inline-block;
margin-right: 10px;
font-size: 0.8em;

View File

@ -1,6 +1,6 @@
import * as classnames from 'classnames';
import * as React from 'react';
import styled, { withProps } from '../styled-components';
import styled from '../styled-components';
const directionMap = {
left: '90deg',
@ -49,7 +49,7 @@ export const ShelfIcon = styled(IntShelfIcon)`
}
`;
export const Badge = withProps<{ type: string }>(styled.span)`
export const Badge = styled.span<{ type: string }>`
display: inline-block;
padding: 0 5px;
margin: 0;

View File

@ -1,4 +1,6 @@
import { darken } from 'polished';
import { Tabs as ReactTabs } from 'react-tabs';
import styled from '../styled-components';
export { Tab, TabList, TabPanel } from 'react-tabs';
@ -14,14 +16,14 @@ export const Tabs = styled(ReactTabs)`
padding: 5px 10px;
display: inline-block;
background-color: rgba(0, 0, 0, 0.2);
background-color: ${({ theme }) => darken(0.05, theme.rightPanel.backgroundColor)};
border-bottom: 1px solid rgba(0, 0, 0, 0.5);
cursor: pointer;
text-align: center;
outline: none;
color: #ccc;
color: ${({ theme }) => darken(theme.colors.tonalOffset, theme.rightPanel.textColor)};
margin: 5px;
border: 1px solid #181f22;
border: 1px solid ${({ theme }) => darken(0.1, theme.rightPanel.backgroundColor)};
border-radius: 5px;
min-width: 60px;
font-size: 0.9em;
@ -29,7 +31,7 @@ export const Tabs = styled(ReactTabs)`
&.react-tabs__tab--selected {
color: ${props => props.theme.colors.text.primary};
background: #e2e2e2;
background: ${({ theme }) => theme.rightPanel.textColor};
}
&:only-child {
@ -55,7 +57,7 @@ export const Tabs = styled(ReactTabs)`
}
}
> .react-tabs__tab-panel {
background: #171e21;
background: ${({ theme }) => theme.codeSample.backgroundColor};
& > div,
& > pre {
padding: 20px;
@ -74,7 +76,7 @@ export const SmallTabs = styled(Tabs)`
font-size: 13px;
font-weight: normal;
border-bottom: 1px dashed;
color: #787b7d;
color: ${({ theme }) => darken(theme.colors.tonalOffset, theme.rightPanel.textColor)};
border-radius: 0;
background: none;
@ -83,7 +85,7 @@ export const SmallTabs = styled(Tabs)`
}
&.react-tabs__tab--selected {
color: #babcbf;
color: ${({ theme }) => theme.rightPanel.textColor};
background: none;
}
}

View File

@ -12,6 +12,8 @@ export class ApiLogo extends React.Component<{ info: OpenAPIInfo }> {
return null;
}
const logoHref = logoInfo.href || (info.contact && info.contact.url);
// Use the english word logo if no alt text is provided
const altText = logoInfo.altText ? logoInfo.altText : 'logo';
@ -25,7 +27,7 @@ export class ApiLogo extends React.Component<{ info: OpenAPIInfo }> {
return (
<LogoWrap className="api-logo">
{info.contact && info.contact.url ? LinkWrap(info.contact.url)(logo) : logo}{' '}
{logoHref ? LinkWrap(logoHref)(logo) : logo}
</LogoWrap>
);
}

View File

@ -1,9 +1,11 @@
import * as React from 'react';
import { ShelfIcon } from '../../common-elements';
import { OperationModel } from '../../services';
import { Markdown } from '../Markdown/Markdown';
import { OptionsContext } from '../OptionsProvider';
import { SelectOnClick } from '../SelectOnClick/SelectOnClick';
import { getBasePath } from '../../utils';
import {
EndpointInfo,
HttpVerb,
@ -60,10 +62,14 @@ export class Endpoint extends React.Component<EndpointProps, EndpointState> {
<ServersOverlay expanded={expanded}>
{operation.servers.map(server => (
<ServerItem key={server.url}>
<div>{server.description}</div>
<Markdown source={server.description || ''} compact={true} />
<SelectOnClick>
<ServerUrl>
{!(hideHostname || options.hideHostname) && <span>{server.url}</span>}
<span>
{hideHostname || options.hideHostname
? getBasePath(server.url)
: server.url}
</span>
{operation.path}
</ServerUrl>
</SelectOnClick>

View File

@ -1,4 +1,4 @@
import styled, { withProps } from '../../styled-components';
import styled from '../../styled-components';
export const OperationEndpointWrap = styled.div`
cursor: pointer;
@ -14,10 +14,11 @@ export const ServerRelativeURL = styled.span`
text-overflow: ellipsis;
`;
export const EndpointInfo = withProps<{ expanded?: boolean; inverted?: boolean }>(styled.div)`
export const EndpointInfo = styled.div<{ expanded?: boolean; inverted?: boolean }>`
padding: 10px 30px 10px ${props => (props.inverted ? '10px' : '20px')};
border-radius: ${props => (props.inverted ? '0' : '4px 4px 0 0')};
background-color: ${props => (props.inverted ? 'transparent' : '#222d32')};
background-color: ${props =>
props.inverted ? 'transparent' : props.theme.codeSample.backgroundColor};
display: flex;
white-space: nowrap;
align-items: center;
@ -33,12 +34,12 @@ export const EndpointInfo = withProps<{ expanded?: boolean; inverted?: boolean }
}
`;
export const HttpVerb = withProps<{ type: string }>(styled.span).attrs({
className: props => `http-verb ${props.type}`,
})`
export const HttpVerb = styled.span.attrs((props: { type: string }) => ({
className: `http-verb ${props.type}`,
}))<{ type: string }>`
font-size: 0.929em;
line-height: 20px;
background-color: ${props => props.theme.colors.http[props.type] || '#999999'};
background-color: ${(props: any) => props.theme.colors.http[props.type] || '#999999'};
color: #ffffff;
padding: 3px 10px;
text-transform: uppercase;
@ -46,7 +47,7 @@ export const HttpVerb = withProps<{ type: string }>(styled.span).attrs({
margin: 0;
`;
export const ServersOverlay = withProps<{ expanded: boolean }>(styled.div)`
export const ServersOverlay = styled.div<{ expanded: boolean }>`
position: absolute;
width: 100%;
z-index: 100;

View File

@ -1,10 +1,10 @@
import { observer } from 'mobx-react';
import * as React from 'react';
import styled, { withProps } from '../../styled-components';
import styled from '../../styled-components';
import { OpenAPIExternalDocumentation } from '../../types';
import { linksCss } from '../Markdown/styled.elements';
const LinkWrap = withProps<{ compact?: boolean }>(styled.div)`
const LinkWrap = styled.div<{ compact?: boolean }>`
${linksCss};
${({ compact }) => (!compact ? 'margin: 1em 0' : '')}
`;

View File

@ -35,7 +35,7 @@ class Json extends React.PureComponent<JsonProps> {
<PrismDiv
className={this.props.className}
// tslint:disable-next-line
innerRef={node => (this.node = node!)}
ref={node => (this.node = node!)}
dangerouslySetInnerHTML={{ __html: jsonToHTML(this.props.data) }}
/>
</JsonViewerWrap>

View File

@ -9,6 +9,7 @@ export const jsonStyles = css`
font-size: ${props => props.theme.typography.code.fontSize};
white-space: ${({ theme }) => (theme.typography.code.wrap ? 'pre-wrap' : 'pre')};
contain: content;
overflow-x: auto;
.callback-function {

View File

@ -1,9 +1,9 @@
import * as React from 'react';
import styled, { withProps } from '../../styled-components';
import styled from '../../styled-components';
import { Spinner } from './Spinner.svg';
const LoadingMessage = withProps<{ color: string }>(styled.div)`
const LoadingMessage = styled.div<{ color: string }>`
font-family: helvetica, sans;
width: 100%;
text-align: center;

View File

@ -1,6 +1,8 @@
import { headerCommonMixin, linkifyMixin } from '../../common-elements';
import { PrismDiv } from '../../common-elements/PrismDiv';
import styled, { css, extensionsHook, withProps } from '../../styled-components';
import styled, { css, extensionsHook, ResolvedThemeInterface } from '../../styled-components';
import { StyledComponent } from 'styled-components';
export const linksCss = css`
a {
@ -17,9 +19,11 @@ export const linksCss = css`
}
`;
export const StyledMarkdownBlock = withProps<{ compact?: boolean; inline?: boolean }>(
styled(PrismDiv),
)`
export const StyledMarkdownBlock = styled(PrismDiv as StyledComponent<
'div',
ResolvedThemeInterface,
{ compact?: boolean; inline?: boolean }
>)`
font-family: ${props => props.theme.typography.fontFamily};
font-weight: ${props => props.theme.typography.fontWeightRegular};
@ -68,6 +72,7 @@ export const StyledMarkdownBlock = withProps<{ compact?: boolean; inline?: boole
border: 1px solid rgba(38, 50, 56, 0.1);
padding: 0.1em 0.25em 0.2em;
font-size: ${props => props.theme.typography.code.fontSize};
font-weight: ${({ theme }) => theme.typography.code.fontWeight};
word-break: break-word;
}
@ -113,9 +118,11 @@ export const StyledMarkdownBlock = withProps<{ compact?: boolean; inline?: boole
padding-left: 2em;
margin: 0;
margin-bottom: 1em;
// > li {
// margin: 0.5em 0;
// }
ul, ol {
margin-bottom: 0;
margin-top: 0;
}
}
table {
@ -135,7 +142,7 @@ export const StyledMarkdownBlock = withProps<{ compact?: boolean; inline?: boole
border-top: 1px solid #ccc;
&:nth-child(2n) {
background-color: #f8f8f8;
background-color: ${({ theme }) => theme.schema.nestedBackground};
}
}

View File

@ -32,7 +32,7 @@ export class MediaTypeSamples extends React.Component<PayloadSamplesProps> {
}
if (examplesNames.length > 1) {
return (
<SmallTabs>
<SmallTabs defaultIndex={0}>
<TabList>
{examplesNames.map(name => (
<Tab key={name}> {examples[name].summary || name} </Tab>

View File

@ -1,3 +1,5 @@
// @ts-ignore
import Dropdown from 'react-dropdown';
import styled from '../../styled-components';
import { StyledDropdown } from '../../common-elements';
@ -13,7 +15,7 @@ export const InvertedSimpleDropdown = styled(StyledDropdown)`
margin-left: 10px;
text-transform: none;
font-size: 0.929em;
border-bottom: 1px solid rgba(255, 255, 255, 0.9);
border-bottom: 1px solid ${({ theme }) => theme.rightPanel.textColor};
margin: 0 0 10px 0;
display: block;
@ -23,11 +25,11 @@ export const InvertedSimpleDropdown = styled(StyledDropdown)`
border: none;
padding: 0 1.2em 0 0;
background: transparent;
color: rgba(255, 255, 255, 0.9);
color: ${({ theme }) => theme.rightPanel.textColor};
box-shadow: none;
.Dropdown-arrow {
border-top-color: rgba(255, 255, 255, 0.9);
border-top-color: ${({ theme }) => theme.rightPanel.textColor};
}
}
.Dropdown-menu {

View File

@ -43,7 +43,14 @@ export const BackgroundStub = styled.div`
top: 0;
bottom: 0;
right: 0;
width: calc((100% - ${({ theme }) => theme.menu.width}) * 0.4);
width: ${({ theme }) => {
if (theme.rightPanel.width.endsWith('%')) {
const percents = parseInt(theme.rightPanel.width, 10);
return `calc((100% - ${theme.menu.width}) * ${percents / 100})`;
} else {
return theme.rightPanel.width;
}
}};
${media.lessThan('medium', true)`
display: none;
`};

View File

@ -1,5 +1,6 @@
import * as React from 'react';
import { darken } from 'polished';
import styled from '../../styled-components';
import { MenuItemLabel } from '../SideMenu/styled.elements';
@ -7,19 +8,20 @@ export const SearchWrap = styled.div`
padding: 5px 0;
`;
export const SearchInput = styled.input.attrs({
export const SearchInput = styled.input.attrs(() => ({
className: 'search-input',
})`
}))`
width: calc(100% - ${props => props.theme.spacing.unit * 8}px);
box-sizing: border-box;
margin: 0 ${props => props.theme.spacing.unit * 4}px;
padding: 5px ${props => props.theme.spacing.unit * 2}px 5px
${props => props.theme.spacing.unit * 4}px;
border: 0;
border-bottom: 1px solid #e1e1e1;
border-bottom: 1px solid ${({ theme }) => darken(0.1, theme.menu.backgroundColor)};
font-family: ${({ theme }) => theme.typography.fontFamily};
font-weight: bold;
font-size: 13px;
color: ${props => props.theme.colors.text};
color: ${props => props.theme.menu.textColor};
background-color: transparent;
outline: none;
`;
@ -44,7 +46,7 @@ export const SearchIcon = styled((props: { className?: string }) => (
width: 0.9em;
path {
fill: ${props => props.theme.colors.text};
fill: ${props => props.theme.menu.textColor};
}
`;

View File

@ -46,7 +46,7 @@ export class MenuItem extends React.Component<MenuItemProps> {
<MenuItemLi
onClick={this.activate}
depth={item.depth}
innerRef={this.saveRef}
ref={this.saveRef}
data-item-id={item.id}
>
{item.type === 'operation' ? (
@ -80,20 +80,14 @@ export class MenuItem extends React.Component<MenuItemProps> {
export interface OperationMenuItemContentProps {
item: OperationModel;
className?: string;
}
@observer
class OperationMenuItemContent extends React.Component<OperationMenuItemContentProps> {
render() {
const { item, className } = this.props;
const { item } = this.props;
return (
<MenuItemLabel
className={className}
depth={item.depth}
active={item.active}
deprecated={item.deprecated}
>
<MenuItemLabel depth={item.depth} active={item.active} deprecated={item.deprecated}>
<OperationBadge type={item.httpVerb}>{shortenHTTPVerb(item.httpVerb)}</OperationBadge>
<MenuItemTitle width="calc(100% - 38px)">
{item.name}

View File

@ -1,11 +1,12 @@
import * as classnames from 'classnames';
import { darken } from 'polished';
import { deprecatedCss, ShelfIcon } from '../../common-elements';
import styled, { css, withProps } from '../../styled-components';
import styled, { css } from '../../styled-components';
export const OperationBadge = withProps<{ type: string }>(styled.span).attrs({
className: props => `operation-type ${props.type}`,
})`
export const OperationBadge = styled.span.attrs((props: { type: string }) => ({
className: `operation-type ${props.type}`,
}))<{ type: string }>`
width: 32px;
display: inline-block;
height: ${props => props.theme.typography.code.fontSize};
@ -57,21 +58,21 @@ export const OperationBadge = withProps<{ type: string }>(styled.span).attrs({
}
&.head {
background-color: ${props => props.theme.colors.http.head};
background-color: ${props => props.theme.colors.http.head};
}
`;
function menuItemActiveBg(depth): string {
function menuItemActiveBg(depth, { theme }): string {
if (depth > 1) {
return '#e1e1e1';
return darken(0.1, theme.menu.backgroundColor);
} else if (depth === 1) {
return '#f0f0f0';
return darken(0.05, theme.menu.backgroundColor);
} else {
return '';
}
}
export const MenuItemUl = withProps<{ expanded: boolean }>(styled.ul)`
export const MenuItemUl = styled.ul<{ expanded: boolean }>`
margin: 0;
padding: 0;
@ -82,7 +83,7 @@ export const MenuItemUl = withProps<{ expanded: boolean }>(styled.ul)`
${props => (props.expanded ? '' : 'display: none;')};
`;
export const MenuItemLi = withProps<{ depth: number }>(styled.li)`
export const MenuItemLi = styled.li<{ depth: number }>`
list-style: none inside none;
overflow: hidden;
text-overflow: ellipsis;
@ -97,7 +98,7 @@ export const menuItemDepth = {
font-size: 0.8em;
padding-bottom: 0;
cursor: default;
color: ${props => props.theme.colors.text.primary};
color: ${props => props.theme.menu.textColor};
`,
1: css`
font-size: 0.929em;
@ -107,27 +108,25 @@ export const menuItemDepth = {
}
`,
2: css`
color: ${props => props.theme.colors.text.primary};
color: ${props => props.theme.menu.textColor};
`,
};
export const MenuItemLabel = withProps<{
export interface MenuItemLabelType {
depth: number;
active: boolean;
deprecated?: boolean;
type?: string;
}>(
styled.label.attrs({
role: 'menuitem',
className: props =>
classnames('-depth' + props.depth, {
active: props.active,
}),
}
export const MenuItemLabel = styled.label.attrs((props: MenuItemLabelType) => ({
role: 'menuitem',
className: classnames('-depth' + props.depth, {
active: props.active,
}),
)`
}))<MenuItemLabelType>`
cursor: pointer;
color: ${props =>
props.active ? props.theme.colors.primary.main : props.theme.colors.text.primary};
color: ${props => (props.active ? props.theme.colors.primary.main : props.theme.menu.textColor)};
margin: 0;
padding: 12.5px ${props => props.theme.spacing.unit * 4}px;
${({ depth, type, theme }) =>
@ -136,12 +135,12 @@ export const MenuItemLabel = withProps<{
justify-content: space-between;
font-family: ${props => props.theme.typography.headings.fontFamily};
${props => menuItemDepth[props.depth]};
background-color: ${props => (props.active ? menuItemActiveBg(props.depth) : '')};
background-color: ${props => (props.active ? menuItemActiveBg(props.depth, props) : '')};
${props => (props.deprecated && deprecatedCss) || ''};
&:hover {
background-color: ${props => menuItemActiveBg(props.depth)};
background-color: ${props => menuItemActiveBg(props.depth, props)};
}
${ShelfIcon} {
@ -153,7 +152,7 @@ export const MenuItemLabel = withProps<{
}
`;
export const MenuItemTitle = withProps<{ width?: string }>(styled.span)`
export const MenuItemTitle = styled.span<{ width?: string }>`
display: inline-block;
vertical-align: middle;
width: ${props => (props.width ? props.width : 'auto')};
@ -173,8 +172,8 @@ export const RedocAttribution = styled.div`
a,
a:visited,
a:hover {
color: ${theme.colors.text.primary} !important;
border-top: 1px solid #e1e1e1;
color: ${theme.menu.textColor} !important;
border-top: 1px solid ${darken(0.1, theme.menu.backgroundColor)};
padding: ${theme.spacing.unit}px 0;
display: block;
}

View File

@ -3,7 +3,7 @@ import * as React from 'react';
import { MenuStore } from '../../services/MenuStore';
import { RedocNormalizedOptions, RedocRawOptions } from '../../services/RedocNormalizedOptions';
import styled, { media, withProps } from '../../styled-components';
import styled, { media } from '../../styled-components';
import { IS_BROWSER } from '../../utils/index';
import { OptionsContext } from '../OptionsProvider';
import { AnimatedChevronButton } from './ChevronSvg';
@ -21,7 +21,7 @@ export interface StickySidebarProps {
const stickyfill = Stickyfill && Stickyfill();
const StyledStickySidebar = withProps<{ open?: boolean }>(styled.div)`
const StyledStickySidebar = styled.div<{ open?: boolean }>`
width: ${props => props.theme.menu.width};
background-color: ${props => props.theme.menu.backgroundColor};
overflow: hidden;
@ -122,7 +122,7 @@ export class StickyResponsiveSidebar extends React.Component<StickySidebarProps>
className={this.props.className}
style={style(options)}
// tslint:disable-next-line
innerRef={el => {
ref={el => {
this.stickyElement = el as any;
}}
>

View File

@ -52,7 +52,14 @@ export class StoreBuilder extends Component<StoreBuilderProps, StoreBuilderState
if (!spec) {
return undefined;
}
return new AppStore(spec, specUrl, options);
try {
return new AppStore(spec, specUrl, options);
} catch (e) {
if (this.props.onLoaded) {
this.props.onLoaded(e);
}
throw e;
}
}
componentDidMount() {

View File

@ -4,7 +4,9 @@ import defaultTheme from '../theme';
export default class TestThemeProvider extends React.Component {
render() {
return <ThemeProvider theme={defaultTheme}>{this.props.children}</ThemeProvider>;
return (
<ThemeProvider theme={defaultTheme}>{React.Children.only(this.props.children)}</ThemeProvider>
);
}
}

View File

@ -1,4 +1,4 @@
import { observe } from 'mobx';
import { Lambda, observe } from 'mobx';
import { OpenAPISpec } from '../types';
import { loadAndBundleSpec } from '../utils/loadAndBundleSpec';
@ -58,7 +58,7 @@ export class AppStore {
marker = new MarkerService();
private scroll: ScrollService;
private disposer;
private disposer: Lambda | null = null;
constructor(
spec: OpenAPISpec,
@ -96,7 +96,9 @@ export class AppStore {
dispose() {
this.scroll.dispose();
this.menu.dispose();
this.disposer();
if (this.disposer != null) {
this.disposer();
}
}
/**

View File

@ -291,6 +291,12 @@ export class OpenAPIParser {
return res;
}
exitParents(shema: MergedOpenAPISchema) {
for (const parent$ref of shema.parentRefs || []) {
this.exitRef({ $ref: parent$ref });
}
}
private hoistOneOfs(schema: OpenAPISchema) {
if (schema.allOf === undefined) {
return schema;
@ -304,9 +310,14 @@ export class OpenAPIParser {
const afterAllOf = allOf.slice(i + 1);
return {
oneOf: sub.oneOf.map(part => {
return this.mergeAllOf({
const merged = this.mergeAllOf({
allOf: [...beforeAllOf, part, ...afterAllOf],
});
// each oneOf should be independent so exiting all the parent refs
// otherwise it will cause false-positive recursive detection
this.exitParents(merged);
return merged;
}),
};
}

View File

@ -126,14 +126,13 @@ export class RedocNormalizedOptions {
allowedMdComponents: Dict<MDXComponentMeta>;
constructor(raw: RedocRawOptions, defaults: RedocRawOptions = {}) {
let hook;
raw = { ...defaults, ...raw };
if (raw.theme && raw.theme.extensionsHook) {
hook = raw.theme.extensionsHook;
raw.theme.extensionsHook = undefined;
}
this.theme = resolveTheme(mergeObjects({} as any, defaultTheme, raw.theme || {}));
this.theme.extensionsHook = hook;
const hook = raw.theme && raw.theme.extensionsHook;
this.theme = resolveTheme(
mergeObjects({} as any, defaultTheme, { ...raw.theme, extensionsHook: undefined }),
);
this.theme.extensionsHook = hook as any;
this.scrollYOffset = RedocNormalizedOptions.normalizeScrollYOffset(raw.scrollYOffset);
this.hideHostname = RedocNormalizedOptions.normalizeHideHostname(raw.hideHostname);

View File

@ -21,15 +21,15 @@ describe('Models', () => {
parser = new OpenAPIParser(spec, undefined, opts);
const schema = new SchemaModel(parser, spec.components.schemas.Test, '', opts);
expect(schema.fields).toHaveLength(3);
const oneOfField = schema.fields[0];
const oneOfField = schema.fields![0];
expect(oneOfField.schema.displayType).toBe('Foo (object) or Bar (object)');
expect(oneOfField.schema.oneOf[0].title).toBe('Foo');
expect(oneOfField.schema.oneOf[1].title).toBe('Bar');
expect(oneOfField.schema.oneOf![0].title).toBe('Foo');
expect(oneOfField.schema.oneOf![1].title).toBe('Bar');
const anyOfField = schema.fields[1];
const anyOfField = schema.fields![1];
expect(anyOfField.schema.displayType).toBe('Foo (object) or Bar (object)');
expect(anyOfField.schema.oneOf[0].title).toBe('Foo');
expect(anyOfField.schema.oneOf[1].title).toBe('Bar');
expect(anyOfField.schema.oneOf![0].title).toBe('Foo');
expect(anyOfField.schema.oneOf![1].title).toBe('Bar');
});
test('oneOf/allOf schema complex displayType', () => {

View File

@ -11,6 +11,9 @@ export class ApiInfoModel implements OpenAPIInfo {
contact?: OpenAPIContact;
license?: OpenAPILicense;
downloadLink?: string;
downloadFileName?: string;
constructor(private parser: OpenAPIParser) {
Object.assign(this, parser.spec.info);
this.description = parser.spec.info.description || '';
@ -18,9 +21,12 @@ export class ApiInfoModel implements OpenAPIInfo {
if (firstHeadingLinePos > -1) {
this.description = this.description.substring(0, firstHeadingLinePos);
}
this.downloadLink = this.getDownloadLink();
this.downloadFileName = this.getDownloadFileName();
}
get downloadLink(): string | undefined {
private getDownloadLink(): string | undefined {
if (this.parser.specUrl) {
return this.parser.specUrl;
}
@ -33,7 +39,7 @@ export class ApiInfoModel implements OpenAPIInfo {
}
}
get downloadFileName(): string | undefined {
private getDownloadFileName(): string | undefined {
if (!this.parser.specUrl) {
return 'swagger.json';
}

View File

@ -75,11 +75,7 @@ export class SchemaModel {
this.init(parser, isChild);
parser.exitRef(schemaOrRef);
for (const parent$ref of this.schema.parentRefs || []) {
// exit all the refs visited during allOf traverse
parser.exitRef({ $ref: parent$ref });
}
parser.exitParents(this.schema);
if (options.showExtensions) {
this.extensions = extractExtensions(this.schema, options.showExtensions);
@ -164,20 +160,28 @@ export class SchemaModel {
}
private initOneOf(oneOf: OpenAPISchema[], parser: OpenAPIParser) {
this.oneOf = oneOf!.map(
(variant, idx) =>
new SchemaModel(
parser,
// merge base schema into each of oneOf's subschemas
{
// variant may already have allOf so merge it to not get overwritten
...parser.mergeAllOf(variant, this.pointer + '/oneOf/' + idx),
allOf: [{ ...this.schema, oneOf: undefined, anyOf: undefined }],
} as OpenAPISchema,
this.pointer + '/oneOf/' + idx,
this.options,
),
);
this.oneOf = oneOf!.map((variant, idx) => {
const merged = parser.mergeAllOf(variant, this.pointer + '/oneOf/' + idx);
const schema = new SchemaModel(
parser,
// merge base schema into each of oneOf's subschemas
{
// variant may already have allOf so merge it to not get overwritten
...merged,
allOf: [{ ...this.schema, oneOf: undefined, anyOf: undefined }],
} as OpenAPISchema,
this.pointer + '/oneOf/' + idx,
this.options,
);
// each oneOf should be independent so exiting all the parent refs
// otherwise it will cause false-positive recursive detection
parser.exitParents(merged);
return schema;
});
this.displayType = this.oneOf
.map(schema => {
let name =

View File

@ -1,24 +1,13 @@
import * as React from 'react';
import * as styledComponents from 'styled-components';
import { ResolvedThemeInterface } from './theme';
export { ResolvedThemeInterface };
export type InterpolationFunction<P> = styledComponents.InterpolationFunction<P>;
export type StyledFunction<T> = styledComponents.ThemedStyledFunction<T, ResolvedThemeInterface>;
function withProps<T, U extends HTMLElement = HTMLElement>(
styledFunction: StyledFunction<React.HTMLProps<U>>,
): StyledFunction<T & React.HTMLProps<U>> {
return styledFunction;
}
const {
default: styled,
css,
injectGlobal,
createGlobalStyle,
keyframes,
ThemeProvider,
} = (styledComponents as any) as styledComponents.ThemedStyledComponentsModule<
@ -54,7 +43,7 @@ export const media = {
},
};
export { css, injectGlobal, keyframes, ThemeProvider, withProps };
export { css, createGlobalStyle, keyframes, ThemeProvider };
export default styled;
export function extensionsHook(styledName: string) {

View File

@ -95,7 +95,7 @@ const defaultTheme: ThemeInterface = {
},
typography: {
fontSize: '14px',
lineHeight: '1.5',
lineHeight: '1.5em',
fontWeightRegular: '400',
fontWeightBold: '600',
fontWeightLight: '300',
@ -104,6 +104,7 @@ const defaultTheme: ThemeInterface = {
optimizeSpeed: true,
headings: {
fontFamily: 'Montserrat, sans-serif',
fontWeight: '400',
},
code: {
fontSize: '13px',
@ -111,18 +112,19 @@ const defaultTheme: ThemeInterface = {
lineHeight: ({ typography }) => typography.lineHeight,
fontWeight: ({ typography }) => typography.fontWeightRegular,
color: '#e53935',
backgroundColor: 'rgba(38, 50, 56, 0.04)',
backgroundColor: 'rgba(38, 50, 56, 0.05)',
wrap: false,
},
links: {
color: ({ colors }) => colors.primary.main,
visited: ({ colors }) => colors.primary.main,
hover: ({ colors }) => lighten(0.2, colors.primary.main),
visited: ({ typography }) => typography.links.color,
hover: ({ typography }) => lighten(0.2, typography.links.color),
},
},
menu: {
width: '260px',
backgroundColor: '#fafafa',
textColor: '#333333',
groupItems: {
textTransform: 'uppercase',
},
@ -131,7 +133,7 @@ const defaultTheme: ThemeInterface = {
},
arrow: {
size: '1.5em',
color: theme => theme.colors.text.primary,
color: theme => theme.menu.textColor,
},
},
logo: {
@ -143,6 +145,9 @@ const defaultTheme: ThemeInterface = {
width: '40%',
textColor: '#ffffff',
},
codeSample: {
backgroundColor: ({ rightPanel }) => darken(0.1, rightPanel.backgroundColor),
},
};
export default defaultTheme;
@ -275,6 +280,7 @@ export interface ResolvedThemeInterface {
};
headings: {
fontFamily: string;
fontWeight: string;
};
links: {
@ -286,6 +292,7 @@ export interface ResolvedThemeInterface {
menu: {
width: string;
backgroundColor: string;
textColor: string;
groupItems: {
textTransform: string;
};
@ -306,6 +313,9 @@ export interface ResolvedThemeInterface {
textColor: string;
width: string;
};
codeSample: {
backgroundColor: string;
};
extensionsHook?: (name: string, props: any) => string;
}

View File

@ -2,6 +2,7 @@ import {
detectType,
getOperationSummary,
getStatusCodeType,
humanizeConstraints,
isOperationName,
isPrimitiveType,
mergeParams,
@ -321,4 +322,35 @@ describe('Utils', () => {
expect(servers[2].url).toEqual('http://127.0.0.3');
});
});
describe('openapi humanizeConstraints', () => {
const itemConstraintSchema = (
min: number | undefined = undefined,
max: number | undefined = undefined,
) => ({ type: 'array', minItems: min, maxItems: max });
it('should not have a humanized constraint without schema constraints', () => {
expect(humanizeConstraints(itemConstraintSchema())).toHaveLength(0);
});
it('should have a humanized constraint when minItems is set', () => {
expect(humanizeConstraints(itemConstraintSchema(2))).toContain('>= 2 items');
});
it('should have a humanized constraint when maxItems is set', () => {
expect(humanizeConstraints(itemConstraintSchema(undefined, 8))).toContain('<= 8 items');
});
it('should have a humanized constraint when minItems and maxItems are both set', () => {
expect(humanizeConstraints(itemConstraintSchema(2, 8))).toContain('[ 2 .. 8 ] items');
});
it('should have a humanized constraint when minItems and maxItems are the same', () => {
expect(humanizeConstraints(itemConstraintSchema(7, 7))).toContain('7 items');
});
it('should have a humazined constraint when justMinItems is set, and it is equal to 1', () => {
expect(humanizeConstraints(itemConstraintSchema(1))).toContain('non-empty');
});
});
});

View File

@ -161,3 +161,7 @@ export function resolveUrl(url: string, to: string) {
}
return stripTrailingSlash(res);
}
export function getBasePath(serverUrl: string): string {
return new URL(serverUrl).pathname;
}

View File

@ -141,29 +141,44 @@ export function isNamedDefinition(pointer?: string): boolean {
return /^#\/components\/schemas\/[^\/]+$/.test(pointer || '');
}
function humanizeRangeConstraint(
description: string,
min: number | undefined,
max: number | undefined,
): string | undefined {
let stringRange;
if (min !== undefined && max !== undefined) {
if (min === max) {
stringRange = `${min} ${description}`;
} else {
stringRange = `[ ${min} .. ${max} ] ${description}`;
}
} else if (max !== undefined) {
stringRange = `<= ${max} ${description}`;
} else if (min !== undefined) {
if (min === 1) {
stringRange = 'non-empty';
} else {
stringRange = `>= ${min} ${description}`;
}
}
return stringRange;
}
export function humanizeConstraints(schema: OpenAPISchema): string[] {
const res: string[] = [];
let stringRange;
if (schema.minLength !== undefined && schema.maxLength !== undefined) {
if (schema.minLength === schema.maxLength) {
stringRange = `${schema.minLength} characters`;
} else {
stringRange = `[ ${schema.minLength} .. ${schema.maxLength} ] characters`;
}
} else if (schema.maxLength !== undefined) {
stringRange = `<= ${schema.maxLength} characters`;
} else if (schema.minLength !== undefined) {
if (schema.minLength === 1) {
stringRange = 'non-empty';
} else {
stringRange = `>= ${schema.minLength} characters`;
}
}
const stringRange = humanizeRangeConstraint('characters', schema.minLength, schema.maxLength);
if (stringRange !== undefined) {
res.push(stringRange);
}
const arrayRange = humanizeRangeConstraint('items', schema.minItems, schema.maxItems);
if (arrayRange !== undefined) {
res.push(arrayRange);
}
let numberRange;
if (schema.minimum !== undefined && schema.maximum !== undefined) {
numberRange = schema.exclusiveMinimum ? '( ' : '[ ';

View File

@ -1,43 +1,21 @@
import * as styledComponents from 'styled-components';
// Styled components typings for using babel-plugin BEFORE typescript
// FIXME
declare module 'styled-components' {
interface ThemedStyledFunction<P, T, O = P> {
// adding "| string[]" for transpileTemplateLiterals and similar below
(
strings: TemplateStringsArray | string[],
...interpolations: Interpolation<ThemedStyledProps<P, T>>[]
): StyledComponentClass<P, T, O>;
new <U>(
strings: TemplateStringsArray | string[],
...interpolations: Interpolation<ThemedStyledProps<P & U, T>>[]
): StyledComponentClass<P & U, T, O & U>;
// adding "withConfig" for transpileTemplateLiterals
withConfig(config: any): ThemedStyledFunction<P, T, O>;
}
export interface ThemedCssFunction<T> {
// adding "| string[]" for transpileTemplateLiterals and similar below
(
strings: TemplateStringsArray | string[],
...interpolations: SimpleInterpolation[]
): InterpolationValue[];
<P>(
strings: TemplateStringsArray | string[],
...interpolations: Interpolation<ThemedStyledProps<P, T>>[]
): FlattenInterpolation<ThemedStyledProps<P, T>>[];
export interface ThemedStyledFunction<
C extends keyof JSX.IntrinsicElements | React.ComponentType<any>,
T extends object,
O extends object = {},
A extends keyof any = never
> extends ThemedStyledFunctionBase<C, T, O, A> {
withConfig(config: any): any;
// tslint:enable:unified-signatures
}
interface ThemedStyledComponentsModule<T> {
keyframes(
strings: TemplateStringsArray | string[],
...interpolations: SimpleInterpolation[]
): string;
injectGlobal(
strings: TemplateStringsArray | string[],
...interpolations: SimpleInterpolation[]
): void;
): Keyframes;
}
}

View File

@ -17,12 +17,18 @@ const nodeExternals = require('webpack-node-externals')({
});
const VERSION = JSON.stringify(require('./package.json').version);
const REVISION = JSON.stringify(
require('child_process')
.execSync('git rev-parse --short HEAD')
.toString()
.trim(),
);
let REVISION;
try {
REVISION = JSON.stringify(
require('child_process')
.execSync('git rev-parse --short HEAD')
.toString()
.trim(),
);
} catch (e) {
console.error('Skipping REDOC_REVISION');
}
const BANNER = `ReDoc - OpenAPI/Swagger-generated API Reference Documentation
-------------------------------------------------------------
@ -106,7 +112,7 @@ export default (env: { standalone?: boolean } = {}, { mode }) => ({
},
},
],
exclude: ['node_modules'],
exclude: [/node_modules/],
},
{
test: /node_modules\/(swagger2openapi|reftools|oas-resolver|oas-kit-common|oas-schema-walker)\/.*\.js$/,

5400
yarn.lock

File diff suppressed because it is too large Load Diff