Merge branch 'Redocly:master' into demo-file

This commit is contained in:
David Goss 2021-08-24 08:33:04 +01:00 committed by GitHub
commit 1b07e16462
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
71 changed files with 32286 additions and 6995 deletions

22
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,22 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: 'type: bug'
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Minimal reproducible OpenAPI snippet(if possible)**
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Additional context**
Add any other context about the problem here.

View File

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Describe the problem to be solved**
A clear and concise description of what problem to be solved
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

13
.github/pull_request_template.md vendored Normal file
View File

@ -0,0 +1,13 @@
## What/Why/How?
## Reference
## Testing
## Screenshots (optional)
## Check yourself
- [ ] Code is linted
- [ ] Tested
- [ ] All new/updated code is covered with tests

6
.github/sync.yml vendored Normal file
View File

@ -0,0 +1,6 @@
group:
- files:
- source: docs/
dest: docs/redoc
repos: |
Redocly/docs

45
.github/workflows/main.yml vendored Normal file
View File

@ -0,0 +1,45 @@
name: Publish Docker image
on:
release:
types: [published]
jobs:
push_to_registry:
name: Push Docker image to GitHub Packages
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- name: Check out the repo
uses: actions/checkout@v2
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Prepare
id: prep
run: |
DOCKER_IMAGE=ghcr.io/redocly/redoc/cli
VERSION=edge
if [[ $GITHUB_REF == refs/tags/* ]]; then
VERSION=${GITHUB_REF#refs/tags/}
elif [[ $GITHUB_REF == refs/heads/* ]]; then
VERSION=$(echo ${GITHUB_REF#refs/heads/} | sed -r 's#/+#-#g')
elif [[ $GITHUB_REF == refs/pull/* ]]; then
VERSION=pr-${{ github.event.number }}
fi
TAGS="${DOCKER_IMAGE}:${VERSION}"
if [ "${{ github.event_name }}" = "push" ]; then
TAGS="$TAGS,${DOCKER_IMAGE}:sha-${GITHUB_SHA::8}"
fi
echo ::set-output name=version::${VERSION}
echo ::set-output name=tags::${TAGS}
echo ::set-output name=created::$(date -u +'%Y-%m-%dT%H:%M:%SZ')
- name: Push to GitHub Packages
uses: docker/build-push-action@v2
with:
context: ./cli
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.prep.outputs.tags }}

18
.github/workflows/sync.yml vendored Normal file
View File

@ -0,0 +1,18 @@
name: Sync Files
on:
push:
branches:
- master
workflow_dispatch:
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@master
- name: Run GitHub File Sync
uses: Redocly/repo-file-sync-action@master
with:
GH_PAT: ${{ secrets.GH_PAT }}
COMMIT_PREFIX: "sync:"
SKIP_PR: true

View File

@ -1,3 +1,57 @@
# [2.0.0-rc.56](https://github.com/Redocly/redoc/compare/v2.0.0-rc.53...v2.0.0-rc.56) (2021-08-11)
### Bug Fixes
* handle empty object in security array ([#1678](https://github.com/Redocly/redoc/issues/1678)) ([9e1ea70](https://github.com/Redocly/redoc/commit/9e1ea703e56a71567b13d0d22e2d69945a22de4d))
* hideLoading options in redoc standalone ([#1709](https://github.com/Redocly/redoc/issues/1709)) ([6a52a16](https://github.com/Redocly/redoc/commit/6a52a16d5b75a2955da7217c4a264f0fa8e98c89))
* improve openapi 3.1 ([#1700](https://github.com/Redocly/redoc/issues/1700)) ([cd2d6f7](https://github.com/Redocly/redoc/commit/cd2d6f76e87c8385786a9c8e51c0d11c79d9707c))
- show contentEncoding on fields
- crash with OpenAPI 3.1 type as array of strings in requestBody
- nullable label not shown
* nullable object's fields were missing ([#1721](https://github.com/Redocly/redoc/issues/1721)) ([ddf297b](https://github.com/Redocly/redoc/commit/ddf297b11269ef515bd62771912a5609721d5e39))
### Features
* add github action to build docker images and push to ghcr.io on release ([#1614](https://github.com/Redocly/redoc/issues/1614)) ([919a5f0](https://github.com/Redocly/redoc/commit/919a5f02fb94ca869011d5eaf63ee71b61b60150))
* add yaml highlight ([#1684](https://github.com/Redocly/redoc/issues/1684)) ([d724440](https://github.com/Redocly/redoc/commit/d72444008533623c87f238fe8758b1dd518b89eb))
* added localization for some labels ([#1675](https://github.com/Redocly/redoc/issues/1675)) ([ec50858](https://github.com/Redocly/redoc/commit/ec50858ec47af08c5fe553266fe3c209fba97eae))
# [2.0.0-rc.55](https://github.com/Redocly/redoc/compare/v2.0.0-rc.54...v2.0.0-rc.55) (2021-07-01)
### Bug Fixes
* broken linkify ([3df72fb](https://github.com/Redocly/redoc/commit/3df72fb99ff24fb9a551565b7568d96f8614ed6f)), closes [#1655](https://github.com/Redocly/redoc/issues/1655)
* fix accidentally removed onLoaded ([b41a8b4](https://github.com/Redocly/redoc/commit/b41a8b4ac714084dc25de7914fa1f99386e907e2)), closes [#1656](https://github.com/Redocly/redoc/issues/1656)
### Features
* added git folder sync config ([a69f0fb](https://github.com/Redocly/redoc/commit/a69f0fb00986a04c812ab273711e8f3501b98139))
# [2.0.0-rc.54](https://github.com/Redocly/redoc/compare/v2.0.0-rc.53...v2.0.0-rc.54) (2021-06-09)
### Bug Fixes
* added missing semicolon to styling ([#1578](https://github.com/Redocly/redoc/issues/1578)) ([dfc4cf1](https://github.com/Redocly/redoc/commit/dfc4cf1caa131aa7bc6da6d489e3a8425d800326))
* parse json theme string for standalone tag ([#1492](https://github.com/Redocly/redoc/issues/1492)) ([d7a0a4d](https://github.com/Redocly/redoc/commit/d7a0a4da17241dd9c089202dba76a8312248616e))
* right absolute path for load and bundle definition ([#1579](https://github.com/Redocly/redoc/issues/1579)) ([ab2d57a](https://github.com/Redocly/redoc/commit/ab2d57a5a2ac5df007d76be0d664f3fb5f909566))
* use operation path if operation summary/description is not provided ([#1596](https://github.com/Redocly/redoc/issues/1596)) ([4b072be](https://github.com/Redocly/redoc/commit/4b072be8d1c0dc4f1fa627168eebaed0a0213e08)), closes [#1270](https://github.com/Redocly/redoc/issues/1270)
### Features
* add basic support OpenAPI 3.1 ([#1622](https://github.com/Redocly/redoc/issues/1622)) ([823be24](https://github.com/Redocly/redoc/commit/823be24b313c3a2445df7e0801a0cc79c20bacd1))
* merge refs oas 3.1 ([#1640](https://github.com/Redocly/redoc/issues/1640)) ([f4ea368](https://github.com/Redocly/redoc/commit/f4ea368f78a693fd70d48b5e0e5ffce3560432f4))
# [2.0.0-rc.51](https://github.com/Redocly/redoc/compare/v2.0.0-rc.50...v2.0.0-rc.51) (2021-04-08) # [2.0.0-rc.51](https://github.com/Redocly/redoc/compare/v2.0.0-rc.50...v2.0.0-rc.51) (2021-04-08)
### Bug Fixes ### Bug Fixes

View File

@ -26,6 +26,7 @@
- The widest OpenAPI v2.0 features support (yes, it supports even `discriminator`) <br> - The widest OpenAPI v2.0 features support (yes, it supports even `discriminator`) <br>
![](docs/images/discriminator-demo.gif) ![](docs/images/discriminator-demo.gif)
- OpenAPI 3.0 support - OpenAPI 3.0 support
- Basic OpenAPI 3.1 support
- Neat **interactive** documentation for nested objects <br> - Neat **interactive** documentation for nested objects <br>
![](docs/images/nested-demo.gif) ![](docs/images/nested-demo.gif)
- Code samples support (via vendor extension) <br> - Code samples support (via vendor extension) <br>
@ -43,7 +44,6 @@
- [x] ~~React rewrite~~ - [x] ~~React rewrite~~
- [x] ~~docs pre-rendering (performance and SEO)~~ - [x] ~~docs pre-rendering (performance and SEO)~~
- [ ] ability to simple branding/styling - [ ] ability to simple branding/styling
- [ ] built-in API Console
## Releases ## Releases
**Important:** all the 2.x releases are deployed to npm and can be used via jsdeliver: **Important:** all the 2.x releases are deployed to npm and can be used via jsdeliver:
@ -58,6 +58,7 @@ Additionally, all the 1.x releases are hosted on our GitHub Pages-based CDN **(d
## Version Guidance ## Version Guidance
| ReDoc Release | OpenAPI Specification | | ReDoc Release | OpenAPI Specification |
|:--------------|:----------------------| |:--------------|:----------------------|
| 2.0.0-alpha.54| 3.1, 3.0.x, 2.0 |
| 2.0.0-alpha.x | 3.0, 2.0 | | 2.0.0-alpha.x | 3.0, 2.0 |
| 1.19.x | 2.0 | | 1.19.x | 2.0 |
| 1.18.x | 2.0 | | 1.18.x | 2.0 |
@ -71,6 +72,7 @@ Additionally, all the 1.x releases are hosted on our GitHub Pages-based CDN **(d
- [Commbox](https://www.commbox.io/api/) - [Commbox](https://www.commbox.io/api/)
- [APIs.guru](https://apis.guru/api-doc/) - [APIs.guru](https://apis.guru/api-doc/)
- [FastAPI](https://github.com/tiangolo/fastapi) - [FastAPI](https://github.com/tiangolo/fastapi)
- [BoxKnight](https://www.docs.boxknight.com/)
## Deployment ## Deployment

View File

@ -3,20 +3,29 @@
**[ReDoc](https://github.com/Redocly/redoc)'s Command Line Interface** **[ReDoc](https://github.com/Redocly/redoc)'s Command Line Interface**
## Installation ## Installation
You can use redoc cli by installing `redoc-cli` globally or using [npx](https://medium.com/@maybekatz/introducing-npx-an-npm-package-runner-55f7d4bd282b).
You can use `redoc-cli` by installing [the package](https://www.npmjs.com/package/redoc-cli) globally,
or using [npx](https://medium.com/@maybekatz/introducing-npx-an-npm-package-runner-55f7d4bd282b).
## Usage ## Usage
Two following commands are available: The two following commands are available:
- `redoc-cli serve [spec]` - starts the server with `spec` rendered with ReDoc. Supports SSR mode (`--ssr`) and can watch the spec (`--watch`) - `redoc-cli serve [spec]` - starts the server with `spec` rendered with ReDoc.
- `redoc-cli bundle [spec]` - bundles spec and ReDoc into **zero-dependency** HTML file. Supports a server-side rendering mode (`--ssr`),
and can watch the spec (`--watch`) to automatically reload the page whenever it changes.
- `redoc-cli bundle [spec]` - bundles `spec` and ReDoc into a **zero-dependency** HTML file.
Some examples: Some examples:
- Bundle with main color changed to `orange`: <br> `$ redoc-cli bundle [spec] --options.theme.colors.primary.main=orange` - Bundle with the main color changed to `orange`:<br/>
- Serve with `nativeScrollbars` option set to true: <br> `$ redoc-cli serve [spec] --options.nativeScrollbars` `$ redoc-cli bundle [spec] --options.theme.colors.primary.main=orange`
- Bundle using custom template (check [default template](https://github.com/Redocly/redoc/blob/master/cli/template.hbs) for reference): <br> `$ redoc-cli bundle [spec] -t custom.hbs` - Serve with the `nativeScrollbars` option set to true:<br/>
- Bundle using custom template and add custom `templateOptions`: <br> `$ redoc-cli bundle [spec] -t custom.hbs --templateOptions.metaDescription "Page meta description"` `$ redoc-cli serve [spec] --options.nativeScrollbars`
- Bundle using a custom [Handlebars](https://handlebarsjs.com/) template
(check the [default template](https://github.com/Redocly/redoc/blob/master/cli/template.hbs) for an example):<br/>
`$ redoc-cli bundle [spec] -t custom.hbs`
- Bundle using a custom template and add custom `templateOptions`:<br/>
`$ redoc-cli bundle [spec] -t custom.hbs --templateOptions.metaDescription "Page meta description"`
For more details run `redoc-cli --help`. For more details, run `redoc-cli --help`.

View File

@ -92,8 +92,6 @@ YargsParser.command(
redocOptions: getObjectOrJSON(argv.options), redocOptions: getObjectOrJSON(argv.options),
}; };
console.log(config);
try { try {
await serve(argv.port as number, argv.spec as string, config); await serve(argv.port as number, argv.spec as string, config);
} catch (e) { } catch (e) {

3375
cli/npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "redoc-cli", "name": "redoc-cli",
"version": "0.11.4", "version": "0.12.3",
"description": "ReDoc's Command Line Interface", "description": "ReDoc's Command Line Interface",
"main": "index.js", "main": "index.js",
"bin": "index.js", "bin": "index.js",
@ -11,18 +11,17 @@
"node": ">=12.0.0" "node": ">=12.0.0"
}, },
"dependencies": { "dependencies": {
"chokidar": "^3.4.1", "chokidar": "^3.5.1",
"handlebars": "^4.7.6", "handlebars": "^4.7.7",
"isarray": "^2.0.5", "isarray": "^2.0.5",
"mkdirp": "^1.0.4", "mkdirp": "^1.0.4",
"mobx": "^6.0.4", "mobx": "^6.3.2",
"node-libs-browser": "^2.2.1", "node-libs-browser": "^2.2.1",
"react": "^16.13.1", "react": "^17.0.1",
"react-dom": "^16.13.1", "react-dom": "^17.0.1",
"redoc": "2.0.0-rc.53", "redoc": "2.0.0-rc.56",
"styled-components": "^5.1.1", "styled-components": "^5.3.0",
"tslib": "^2.0.0", "yargs": "^17.0.1"
"yargs": "^15.4.1"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"

View File

@ -17,6 +17,7 @@ RUN npm ci --no-optional --ignore-scripts
# copy only required for the build files # copy only required for the build files
COPY src /build/src COPY src /build/src
COPY webpack.config.ts tsconfig.json custom.d.ts /build/ COPY webpack.config.ts tsconfig.json custom.d.ts /build/
COPY config/webpack-utils.ts /build/config/
COPY typings/styled-patch.d.ts /build/typings/styled-patch.d.ts COPY typings/styled-patch.d.ts /build/typings/styled-patch.d.ts
RUN npm run bundle:standalone RUN npm run bundle:standalone

46
config/webpack-utils.ts Normal file
View File

@ -0,0 +1,46 @@
import * as webpack from 'webpack';
export function getBabelLoader({useBuiltIns, hot}: {useBuiltIns: boolean, hot?: boolean}) {
return {
loader: 'babel-loader',
options: {
babelrc: false,
sourceType: 'unambiguous',
presets: [
[
'@babel/preset-env',
{
useBuiltIns: useBuiltIns ? 'usage' : false,
corejs: 3,
exclude: ['transform-typeof-symbol'],
targets: 'defaults',
modules: false,
},
],
['@babel/preset-react', { development: false, runtime: 'classic' }],
'@babel/preset-typescript',
],
plugins: [
['@babel/plugin-proposal-decorators', { legacy: true }],
['@babel/plugin-proposal-class-properties', { loose: false }],
[
'@babel/plugin-transform-runtime',
{
corejs: false,
helpers: true,
// eslint-disable-next-line import/no-internal-modules
version: require('@babel/runtime/package.json').version,
regenerator: true,
},
],
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-proposal-nullish-coalescing-operator',
hot ? 'react-hot-loader/babel' : undefined,
].filter(Boolean)
},
};
}
export function webpackIgnore(regexp) {
return new webpack.NormalModuleReplacementPlugin(regexp, require.resolve('lodash/noop.js'));
}

View File

@ -31,7 +31,7 @@ const DropDownList = styled.ul`
list-style: none; list-style: none;
margin: 4px 0 0 0; margin: 4px 0 0 0;
padding: 5px 0; padding: 5px 0;
font-family: 'Lato'; font-family: Roboto,sans-serif;
overflow: hidden; overflow: hidden;
`; `;

View File

@ -6,19 +6,21 @@ import { RedocStandalone } from '../src';
import ComboBox from './ComboBox'; import ComboBox from './ComboBox';
import ClipboardImporter from './ClipboardImporter'; import ClipboardImporter from './ClipboardImporter';
const DEFAULT_SPEC = 'openapi.yaml';
const NEW_VERSION_SPEC = 'openapi-3-1.yaml';
const demos = [ const demos = [
{ value: NEW_VERSION_SPEC, label: 'Petstore OpenAPI 3.1' },
{ value: 'https://api.apis.guru/v2/specs/instagram.com/1.0.0/swagger.yaml', label: 'Instagram' }, { value: 'https://api.apis.guru/v2/specs/instagram.com/1.0.0/swagger.yaml', label: 'Instagram' },
{ {
value: 'https://api.apis.guru/v2/specs/googleapis.com/calendar/v3/openapi.yaml', value: 'https://api.apis.guru/v2/specs/googleapis.com/calendar/v3/openapi.yaml',
label: 'Google Calendar', label: 'Google Calendar',
}, },
{ value: 'https://api.apis.guru/v2/specs/slack.com/1.5.0/openapi.yaml', label: 'Slack' }, { value: 'https://api.apis.guru/v2/specs/slack.com/1.7.0/openapi.yaml', label: 'Slack' },
{ value: 'https://api.apis.guru/v2/specs/zoom.us/2.0.0/swagger.yaml', label: 'Zoom.us' }, { value: 'https://api.apis.guru/v2/specs/zoom.us/2.0.0/openapi.yaml', label: 'Zoom.us' },
{ value: 'https://docs.graphhopper.com/openapi.json', label: 'GraphHopper' }, { value: 'https://docs.graphhopper.com/openapi.json', label: 'GraphHopper' },
]; ];
const DEFAULT_SPEC = 'openapi.yaml';
class DemoApp extends React.Component< class DemoApp extends React.Component<
{}, {},
{ spec?: object; specUrl: string; dropdownOpen: boolean; cors: boolean } { spec?: object; specUrl: string; dropdownOpen: boolean; cors: boolean }
@ -54,6 +56,9 @@ class DemoApp extends React.Component<
}; };
handleChange = (url: string) => { handleChange = (url: string) => {
if (url === NEW_VERSION_SPEC) {
this.setState({ cors: false })
}
this.setState({ this.setState({
spec: undefined, spec: undefined,
specUrl: url, specUrl: url,
@ -82,7 +87,7 @@ class DemoApp extends React.Component<
let proxiedUrl = specUrl; let proxiedUrl = specUrl;
if (specUrl !== DEFAULT_SPEC) { if (specUrl !== DEFAULT_SPEC) {
proxiedUrl = cors proxiedUrl = cors
? '\\\\cors.apis.guru/' + urlResolve(window.location.href, specUrl) ? '\\\\cors.redoc.ly/' + urlResolve(window.location.href, specUrl)
: specUrl; : specUrl;
} }
return ( return (
@ -161,7 +166,7 @@ const Heading = styled.nav`
display: flex; display: flex;
align-items: center; align-items: center;
font-family: 'Lato'; font-family: Roboto, sans-serif;
`; `;
const Logo = styled.img` const Logo = styled.img`

1247
demo/openapi-3-1.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import { render } from 'react-dom'; import { render } from 'react-dom';
// tslint:disable-next-line import type { RedocRawOptions } from '../../src/services/RedocNormalizedOptions';
import { AppContainer } from 'react-hot-loader'; import RedocStandalone from './hot';
// import DevTools from 'mobx-react-devtools';
import { Redoc, RedocProps } from '../../src/components/Redoc/Redoc';
import { AppStore } from '../../src/services/AppStore';
import { RedocRawOptions } from '../../src/services/RedocNormalizedOptions';
import { loadAndBundleSpec } from '../../src/utils/loadAndBundleSpec';
const renderRoot = (props: RedocProps) =>
render(
<AppContainer>
<Redoc {...props} />
</AppContainer>,
document.getElementById('example'),
);
const big = window.location.search.indexOf('big') > -1; const big = window.location.search.indexOf('big') > -1;
const swagger = window.location.search.indexOf('swagger') > -1; const swagger = window.location.search.indexOf('swagger') > -1;
@ -25,30 +11,6 @@ const userUrl = window.location.search.match(/url=(.*)$/);
const specUrl = const specUrl =
(userUrl && userUrl[1]) || (swagger ? 'swagger.yaml' : big ? 'big-openapi.json' : 'openapi.yaml'); (userUrl && userUrl[1]) || (swagger ? 'swagger.yaml' : big ? 'big-openapi.json' : 'openapi.yaml');
let store;
const options: RedocRawOptions = { nativeScrollbars: false, maxDisplayedEnumValues: 3 }; const options: RedocRawOptions = { nativeScrollbars: false, maxDisplayedEnumValues: 3 };
async function init() { render(<RedocStandalone specUrl={specUrl} options={options} />, document.getElementById('example'));
const spec = await loadAndBundleSpec(specUrl);
store = new AppStore(spec, specUrl, options);
renderRoot({ store });
}
init();
if (module.hot) {
const reload = (reloadStore = false) => async () => {
if (reloadStore) {
// create a new Store
store.dispose();
const state = await store.toJS();
store = AppStore.fromJS(state);
}
renderRoot({ store });
};
module.hot.accept(['../../src/components/Redoc/Redoc'], reload());
module.hot.accept(['../../src/services/AppStore'], reload(true));
}

10
demo/playground/hot.tsx Normal file
View File

@ -0,0 +1,10 @@
import * as React from 'react';
// eslint-disable-next-line import/no-internal-modules
import { hot } from 'react-hot-loader/root';
import { RedocStandalone as RedocStandaloneOrig, RedocStandaloneProps } from '../../src';
const RedocStandalone = function (props: RedocStandaloneProps) {
return <RedocStandaloneOrig {...props} />;
}
export default hot(RedocStandalone);

View File

@ -1,14 +1,12 @@
import { renderToString } from 'react-dom/server'; import { renderToString } from 'react-dom/server';
import * as React from 'react'; import * as React from 'react';
import { ServerStyleSheet } from 'styled-components'; import { ServerStyleSheet } from 'styled-components';
// @ts-ignore
import { Redoc, createStore } from '../../'; import { Redoc, createStore } from '../../';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { resolve } from 'path'; import { resolve } from 'path';
const yaml = require('yaml-js'); const yaml = require('js-yaml');
const http = require('http'); const http = require('http');
const url = require('url');
const fs = require('fs'); const fs = require('fs');
const PORT = 9999; const PORT = 9999;
@ -18,8 +16,8 @@ const server = http.createServer(async (request, response) => {
if (request.url === '/redoc.standalone.js') { if (request.url === '/redoc.standalone.js') {
fs.createReadStream('bundles/redoc.standalone.js', 'utf8').pipe(response); fs.createReadStream('bundles/redoc.standalone.js', 'utf8').pipe(response);
} else if (request.url === '/') { } else if (request.url === '/') {
const spec = yaml.load(readFileSync(resolve(__dirname, '../openapi.yaml'))); const spec = yaml.load(readFileSync(resolve(__dirname, '../openapi.yaml'), 'utf-8'));
let store = await createStore(spec, 'path/to/spec.yaml'); const store = await createStore(spec, 'path/to/spec.yaml');
const sheet = new ServerStyleSheet(); const sheet = new ServerStyleSheet();

View File

@ -1,9 +1,9 @@
import * as CopyWebpackPlugin from 'copy-webpack-plugin'; import * as CopyWebpackPlugin from 'copy-webpack-plugin';
import ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); import ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
import * as HtmlWebpackPlugin from 'html-webpack-plugin'; import * as HtmlWebpackPlugin from 'html-webpack-plugin';
import { compact } from 'lodash';
import { resolve } from 'path'; import { resolve } from 'path';
import * as webpack from 'webpack'; import * as webpack from 'webpack';
import { getBabelLoader, webpackIgnore } from '../config/webpack-utils';
const VERSION = JSON.stringify(require('../package.json').version); const VERSION = JSON.stringify(require('../package.json').version);
const REVISION = JSON.stringify( const REVISION = JSON.stringify(
@ -14,38 +14,6 @@ function root(filename) {
return resolve(__dirname + '/' + filename); return resolve(__dirname + '/' + filename);
} }
const tsLoader = (env) => ({
loader: 'ts-loader',
options: {
compilerOptions: {
module: env.bench ? 'esnext' : 'es2015',
declaration: false,
},
},
});
const babelLoader = () => ({
loader: 'babel-loader',
options: {
generatorOpts: {
decoratorsBeforeExport: true,
},
plugins: compact([
['@babel/plugin-syntax-typescript', { isTSX: true }],
['@babel/plugin-syntax-decorators', { legacy: true }],
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-syntax-jsx',
]),
},
});
const babelHotLoader = {
loader: 'babel-loader',
options: {
plugins: ['react-hot-loader/babel'],
},
};
export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) => ({ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) => ({
entry: [ entry: [
root('../src/polyfills.ts'), root('../src/polyfills.ts'),
@ -57,6 +25,7 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
: 'index.tsx', : 'index.tsx',
), ),
], ],
target: 'web',
output: { output: {
filename: 'redoc-demo.bundle.js', filename: 'redoc-demo.bundle.js',
path: root('dist'), path: root('dist'),
@ -69,10 +38,17 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
port: 9090, port: 9090,
disableHostCheck: true, disableHostCheck: true,
stats: 'minimal', stats: 'minimal',
hot: true,
}, },
resolve: { resolve: {
extensions: ['.ts', '.tsx', '.js', '.json'], extensions: ['.ts', '.tsx', '.js', '.json'],
fallback: {
path: require.resolve('path-browserify'),
http: false,
fs: false,
os: false,
},
alias: alias:
mode !== 'production' mode !== 'production'
? { ? {
@ -81,10 +57,6 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
: {}, : {},
}, },
node: {
fs: 'empty',
},
performance: false, performance: false,
externals: { externals: {
@ -97,16 +69,26 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
module: { module: {
rules: [ rules: [
{ enforce: 'pre', test: /\.js$/, loader: 'source-map-loader' },
{ test: [/\.eot$/, /\.gif$/, /\.woff$/, /\.svg$/, /\.ttf$/], use: 'null-loader' }, { test: [/\.eot$/, /\.gif$/, /\.woff$/, /\.svg$/, /\.ttf$/], use: 'null-loader' },
{ {
test: /\.tsx?$/, test: /\.tsx?$/,
use: compact([ use: [getBabelLoader({useBuiltIns: true, hot: true} )],
mode !== 'production' ? babelHotLoader : undefined, exclude: {
tsLoader(env), and: [/node_modules/],
babelLoader(), not: {
]), or: [
exclude: [/node_modules/], /swagger2openapi/,
/reftools/,
/openapi-sampler/,
/mobx/,
/oas-resolver/,
/oas-kit-common/,
/oas-schema-walker/,
/\@redocly\/openapi-core/,
/colorette/,
],
},
},
}, },
{ {
test: /\.css$/, test: /\.css$/,
@ -117,29 +99,18 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
}, },
}, },
}, },
{
test: /node_modules\/(swagger2openapi|reftools|oas-resolver|oas-kit-common|oas-schema-walker)\/.*\.js$/,
use: {
loader: 'ts-loader',
options: {
transpileOnly: true,
instance: 'ts2js-transpiler-only',
compilerOptions: {
allowJs: true,
declaration: false,
},
},
},
},
], ],
}, },
plugins: [ plugins: [
new webpack.DefinePlugin({ new webpack.DefinePlugin({
__REDOC_VERSION__: VERSION, __REDOC_VERSION__: VERSION,
__REDOC_REVISION__: REVISION, __REDOC_REVISION__: REVISION,
'process.env': '{}',
'process.platform': '"browser"',
'process.stdout': 'null',
}), }),
new webpack.NamedModulesPlugin(), // new webpack.NamedModulesPlugin(),
new webpack.optimize.ModuleConcatenationPlugin(), // new webpack.optimize.ModuleConcatenationPlugin(),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
template: env.playground template: env.playground
? 'demo/playground/index.html' ? 'demo/playground/index.html'
@ -147,16 +118,12 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
? 'benchmark/index.html' ? 'benchmark/index.html'
: 'demo/index.html', : 'demo/index.html',
}), }),
new ForkTsCheckerWebpackPlugin(), new ForkTsCheckerWebpackPlugin({ logger: { infrastructure: 'silent', issues: 'console' } }),
ignore(/js-yaml\/dumper\.js$/), webpackIgnore(/js-yaml\/dumper\.js$/),
ignore(/json-schema-ref-parser\/lib\/dereference\.js/), webpackIgnore(/json-schema-ref-parser\/lib\/dereference\.js/),
ignore(/^\.\/SearchWorker\.worker$/), webpackIgnore(/^\.\/SearchWorker\.worker$/),
new CopyWebpackPlugin({ new CopyWebpackPlugin({
patterns: ['demo/openapi.yaml'], patterns: ['demo/openapi.yaml'],
}), }),
], ],
}); });
function ignore(regexp) {
return new webpack.NormalModuleReplacementPlugin(regexp, require.resolve('lodash/noop.js'));
}

View File

@ -1,5 +1,5 @@
// tslint:disable:no-implicit-dependencies // tslint:disable:no-implicit-dependencies
import * as yaml from 'yaml-js'; import * as yaml from 'js-yaml';
async function loadSpec(url: string): Promise<any> { async function loadSpec(url: string): Promise<any> {
const spec = await (await fetch(url)).text(); const spec = await (await fetch(url)).text();

View File

@ -16,5 +16,6 @@ describe('Standalone bundle test', () => {
} }
baseCheck('OAS3 mode', 'e2e/standalone.html'); baseCheck('OAS3 mode', 'e2e/standalone.html');
baseCheck('OAS3.1 mode', 'e2e/standalone-3-1.html');
baseCheck('OAS2 compatibility mode', 'e2e/standalone-compatibility.html'); baseCheck('OAS2 compatibility mode', 'e2e/standalone-compatibility.html');
}); });

8
e2e/standalone-3-1.html Normal file
View File

@ -0,0 +1,8 @@
<html>
<body>
<redoc spec-url="../demo/openapi-3-1.yaml" native-scrollbars></redoc>
<script src="../bundles/redoc.standalone.js"></script>
</body>
</html>

31195
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,15 @@
{ {
"name": "redoc", "name": "redoc",
"version": "2.0.0-rc.53", "version": "2.0.0-rc.56",
"description": "ReDoc", "description": "ReDoc",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/Redocly/redoc" "url": "git://github.com/Redocly/redoc"
}, },
"browserslist": [
"defaults",
"ie 11"
],
"engines": { "engines": {
"node": ">=6.9", "node": ">=6.9",
"npm": ">=3.0.0" "npm": ">=3.0.0"
@ -25,26 +29,27 @@
"main": "bundles/redoc.lib.js", "main": "bundles/redoc.lib.js",
"types": "typings/index.d.ts", "types": "typings/index.d.ts",
"scripts": { "scripts": {
"start": "webpack-dev-server --mode=development --env.playground --hot --config demo/webpack.config.ts", "start": "webpack serve --mode=development --env playground --hot --config demo/webpack.config.ts",
"start:prod": "webpack-dev-server --env.playground --mode=production --config demo/webpack.config.ts", "start:prod": "webpack serve --env playground --mode=production --config demo/webpack.config.ts",
"start:benchmark": "webpack-dev-server --mode=production --env.bench --config demo/webpack.config.ts", "start:benchmark": "webpack serve --mode=production --env.bench --config demo/webpack.config.ts",
"test": "npm run lint && npm run unit && npm run license-check", "test": "npm run lint && npm run unit && npm run license-check",
"unit": "jest --coverage", "unit": "jest --coverage",
"e2e": "cypress run", "e2e": "cypress run",
"e2e-ci": "cypress run --record", "e2e-ci": "cypress run --record",
"bundlesize": "bundlesize", "bundlesize": "bundlesize",
"ts-check": "tsc --noEmit --skipLibCheck",
"cy:open": "cypress open", "cy:open": "cypress open",
"bundle:clean": "rimraf bundles", "bundle:clean": "rimraf bundles",
"bundle:standalone": "webpack --env.standalone --mode=production", "bundle:standalone": "webpack --env production --env standalone --mode=production",
"bundle:lib": "webpack --mode=production && npm run declarations", "bundle:lib": "webpack --mode=production && npm run declarations",
"bundle": "npm run bundle:clean && npm run bundle:lib && npm run bundle:standalone", "bundle": "npm run bundle:clean && npm run bundle:lib && npm run bundle:standalone",
"declarations": "tsc --emitDeclarationOnly -p tsconfig.lib.json && cp -R src/types typings/", "declarations": "tsc --emitDeclarationOnly -p tsconfig.lib.json && cp -R src/types typings/",
"stats": "webpack --env.standalone --json --profile --mode=production > stats.json", "stats": "webpack --env production --env standalone --json --profile --mode=production > stats.json",
"prettier": "prettier --write \"cli/index.ts\" \"src/**/*.{ts,tsx}\"", "prettier": "prettier --write \"cli/index.ts\" \"src/**/*.{ts,tsx}\"",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 1", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 1",
"lint": "eslint 'src/**/*.{js,ts,tsx}'", "lint": "eslint 'src/**/*.{js,ts,tsx}'",
"benchmark": "node ./benchmark/benchmark.js", "benchmark": "node ./benchmark/benchmark.js",
"start:demo": "webpack-dev-server --hot --config demo/webpack.config.ts --mode=development", "start:demo": "webpack serve --hot --config demo/webpack.config.ts --mode=development",
"compile:cli": "tsc custom.d.ts cli/index.ts --target es6 --module commonjs --types yargs", "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", "build:demo": "webpack --mode=production --config demo/webpack.config.ts",
"deploy:demo": "aws s3 sync demo/dist s3://production-redoc-demo --acl=public-read", "deploy:demo": "aws s3 sync demo/dist s3://production-redoc-demo --acl=public-read",
@ -52,120 +57,128 @@
"docker:build": "docker build -f config/docker/Dockerfile -t redoc ." "docker:build": "docker build -f config/docker/Dockerfile -t redoc ."
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.10.5", "@babel/core": "^7.14.3",
"@babel/plugin-syntax-decorators": "^7.10.4", "@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/plugin-proposal-decorators": "^7.14.2",
"@babel/plugin-proposal-object-rest-spread": "^7.14.4",
"@babel/plugin-syntax-decorators": "^7.12.13",
"@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-jsx": "^7.10.4", "@babel/plugin-syntax-jsx": "^7.10.4",
"@babel/plugin-syntax-typescript": "^7.10.4", "@babel/plugin-syntax-typescript": "^7.10.4",
"@cypress/webpack-preprocessor": "^5.4.2", "@babel/plugin-transform-runtime": "^7.14.3",
"@hot-loader/react-dom": "^16.12.0", "@babel/preset-env": "^7.14.4",
"@types/chai": "^4.2.12", "@babel/preset-react": "^7.13.13",
"@types/dompurify": "^2.0.2", "@babel/preset-typescript": "^7.13.0",
"@cypress/webpack-preprocessor": "^5.9.0",
"@hot-loader/react-dom": "^17.0.1",
"@types/chai": "^4.2.18",
"@types/dompurify": "^2.2.2",
"@types/enzyme": "^3.10.5", "@types/enzyme": "^3.10.5",
"@types/enzyme-to-json": "^1.5.3", "@types/enzyme-to-json": "^1.5.3",
"@types/jest": "^26.0.7", "@types/jest": "^26.0.23",
"@types/json-pointer": "^1.0.30", "@types/json-pointer": "^1.0.30",
"@types/lodash": "^4.14.158", "@types/lodash": "^4.14.170",
"@types/lunr": "^2.3.3", "@types/lunr": "^2.3.3",
"@types/mark.js": "^8.11.5", "@types/mark.js": "^8.11.5",
"@types/marked": "^1.1.0", "@types/marked": "^1.1.0",
"@types/prismjs": "^1.16.1", "@types/prismjs": "^1.16.5",
"@types/prop-types": "^15.7.3", "@types/prop-types": "^15.7.3",
"@types/react": "^16.9.43", "@types/react": "^17.0.8",
"@types/react-dom": "^16.9.8", "@types/react-dom": "^17.0.5",
"@types/react-tabs": "^2.3.2", "@types/react-tabs": "^2.3.2",
"@types/styled-components": "^5.1.1", "@types/styled-components": "^5.1.1",
"@types/tapable": "^1.0.6", "@types/tapable": "^2.2.2",
"@types/webpack": "^4.41.21", "@types/webpack": "^5.28.0",
"@types/webpack-env": "^1.15.2", "@types/webpack-env": "^1.16.0",
"@types/yargs": "^15.0.5", "@types/yargs": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^3.7.0", "@typescript-eslint/eslint-plugin": "^4.26.0",
"@typescript-eslint/parser": "^3.7.0", "@typescript-eslint/parser": "^4.26.0",
"babel-loader": "^8.1.0", "@wojtekmaj/enzyme-adapter-react-17": "^0.6.1",
"babel-plugin-styled-components": "^1.10.7", "babel-loader": "^8.2.2",
"babel-plugin-styled-components": "^1.12.0",
"beautify-benchmark": "^0.2.4", "beautify-benchmark": "^0.2.4",
"bundlesize": "^0.18.0", "bundlesize": "^0.18.1",
"conventional-changelog-cli": "^2.0.34", "conventional-changelog-cli": "^2.0.34",
"copy-webpack-plugin": "^6.0.3", "copy-webpack-plugin": "^9.0.0",
"core-js": "^3.6.5", "core-js": "^3.13.1",
"coveralls": "^3.1.0", "coveralls": "^3.1.0",
"css-loader": "^3.6.0", "css-loader": "^5.2.6",
"cypress": "^4.11.0", "cypress": "^7.4.0",
"enzyme": "^3.11.0", "enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2", "enzyme-to-json": "^3.6.2",
"enzyme-to-json": "^3.5.0", "eslint": "^7.27.0",
"eslint": "^7.5.0", "eslint-plugin-import": "^2.23.4",
"eslint-plugin-import": "^2.22.0", "eslint-plugin-react": "^7.24.0",
"eslint-plugin-react": "^7.20.3", "fork-ts-checker-webpack-plugin": "^6.2.10",
"fork-ts-checker-webpack-plugin": "^5.0.11", "html-webpack-plugin": "^5.3.1",
"html-webpack-plugin": "^4.3.0", "jest": "^27.0.3",
"jest": "^26.1.0", "js-yaml": "^4.1.0",
"license-checker": "^25.0.1", "license-checker": "^25.0.1",
"lodash": "^4.17.19", "lodash": "^4.17.21",
"mobx": "^6.0.4", "mobx": "^6.3.2",
"prettier": "^2.0.5", "prettier": "^2.3.0",
"raf": "^3.4.1", "raf": "^3.4.1",
"react": "^16.13.1", "react": "^17.0.2",
"react-dom": "^16.13.1", "react-dom": "^17.0.2",
"react-hot-loader": "^4.12.21", "react-hot-loader": "^4.13.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"shelljs": "^0.8.4", "shelljs": "^0.8.4",
"source-map-loader": "^1.0.1", "style-loader": "^2.0.0",
"style-loader": "^1.2.1", "styled-components": "^5.3.0",
"styled-components": "^5.1.1", "ts-jest": "^27.0.2",
"ts-jest": "^26.1.3",
"ts-loader": "^8.0.1", "ts-loader": "^8.0.1",
"ts-node": "^8.10.2", "ts-node": "^10.0.0",
"typescript": "^3.9.7", "typescript": "~4.1.0",
"unfetch": "^4.1.0", "unfetch": "^4.2.0",
"url-polyfill": "^1.1.10", "url-polyfill": "^1.1.12",
"webpack": "^4.44.0", "webpack": "^5.38.1",
"webpack-cli": "^3.3.12", "webpack-cli": "^4.7.2",
"webpack-dev-server": "^3.11.0", "webpack-dev-server": "^3.11.2",
"webpack-node-externals": "^2.5.0", "webpack-node-externals": "^3.0.0",
"workerize-loader": "^1.3.0", "workerize-loader": "github:redocly/workerize-loader#webpack-5-dist"
"yaml-js": "^0.2.3"
}, },
"peerDependencies": { "peerDependencies": {
"core-js": "^3.1.4", "core-js": "^3.1.4",
"mobx": "^6.0.4", "mobx": "^6.0.4",
"react": "^16.8.4", "react": "^16.8.4 || ^17.0.0",
"react-dom": "^16.8.4", "react-dom": "^16.8.4 || ^17.0.0",
"styled-components": "^4.1.1 || ^5.1.1" "styled-components": "^4.1.1 || ^5.1.1"
}, },
"dependencies": { "dependencies": {
"@redocly/openapi-core": "^1.0.0-beta.44", "@babel/runtime": "^7.14.0",
"@redocly/openapi-core": "^1.0.0-beta.50",
"@redocly/react-dropdown-aria": "^2.0.11", "@redocly/react-dropdown-aria": "^2.0.11",
"@types/node": "^13.11.1", "@types/node": "^15.6.1",
"classnames": "^2.2.6", "classnames": "^2.3.1",
"decko": "^1.2.0", "decko": "^1.2.0",
"dompurify": "^2.0.12", "dompurify": "^2.2.8",
"eventemitter3": "^4.0.4", "eventemitter3": "^4.0.7",
"json-pointer": "^0.6.0", "json-pointer": "^0.6.1",
"lunr": "2.3.8", "lunr": "^2.3.9",
"mark.js": "^8.11.1", "mark.js": "^8.11.1",
"marked": "^0.7.0", "marked": "^0.7.0",
"memoize-one": "~5.1.1", "memoize-one": "^5.2.1",
"mobx-react": "^7.0.5", "mobx-react": "^7.2.0",
"openapi-sampler": "^1.0.0-beta.18", "openapi-sampler": "^1.0.1",
"perfect-scrollbar": "^1.4.0", "path-browserify": "^1.0.1",
"polished": "^3.6.5", "perfect-scrollbar": "^1.5.1",
"prismjs": "^1.22.0", "polished": "^4.1.3",
"prismjs": "^1.24.1",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react-tabs": "^3.1.1", "react-tabs": "^3.2.2",
"slugify": "^1.4.4", "slugify": "~1.4.7",
"stickyfill": "^1.1.1", "stickyfill": "^1.1.1",
"swagger2openapi": "^6.2.1", "swagger2openapi": "^7.0.6",
"tslib": "^2.0.0",
"url-template": "^2.0.8" "url-template": "^2.0.8"
}, },
"bundlesize": [ "bundlesize": [
{ {
"path": "./bundles/redoc.standalone.js", "path": "./bundles/redoc.standalone.js",
"maxSize": "300 kB" "maxSize": "350 kB"
} }
], ],
"jest": { "jest": {
"testEnvironment": "jsdom",
"setupFilesAfterEnv": [ "setupFilesAfterEnv": [
"<rootDir>/src/setupTests.ts" "<rootDir>/src/setupTests.ts"
], ],

View File

@ -2,7 +2,7 @@
import * as React from 'react'; import * as React from 'react';
import { renderToString } from 'react-dom/server'; import { renderToString } from 'react-dom/server';
import * as yaml from 'yaml-js'; import * as yaml from 'js-yaml';
import { createStore, Redoc } from '../'; import { createStore, Redoc } from '../';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
@ -10,7 +10,7 @@ import { resolve } from 'path';
describe('SSR', () => { describe('SSR', () => {
it('should render in SSR mode', async () => { it('should render in SSR mode', async () => {
const spec = yaml.load(readFileSync(resolve(__dirname, '../../demo/openapi.yaml'))); const spec = yaml.load(readFileSync(resolve(__dirname, '../../demo/openapi.yaml'), 'utf-8'));
const store = await createStore(spec, ''); const store = await createStore(spec, '');
expect(() => { expect(() => {
renderToString(<Redoc store={store} />); renderToString(<Redoc store={store} />);

View File

@ -1,7 +1,7 @@
/* tslint:disable:no-implicit-dependencies */ /* tslint:disable:no-implicit-dependencies */
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import * as React from 'react'; import * as React from 'react';
import * as yaml from 'yaml-js'; import * as yaml from 'js-yaml';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { resolve } from 'path'; import { resolve } from 'path';
@ -11,7 +11,7 @@ import { Loading, RedocStandalone } from '../components/';
describe('Components', () => { describe('Components', () => {
describe('RedocStandalone', () => { describe('RedocStandalone', () => {
test('should show loading first', () => { test('should show loading first', () => {
const spec = yaml.load(readFileSync(resolve(__dirname, '../../demo/openapi.yaml'))); const spec = yaml.load(readFileSync(resolve(__dirname, '../../demo/openapi.yaml'), 'utf-8'));
const inst = mount(<RedocStandalone spec={spec} options={{}} />); const inst = mount(<RedocStandalone spec={spec} options={{}} />);
expect(inst.find(Loading)).toHaveLength(1); expect(inst.find(Loading)).toHaveLength(1);

View File

@ -61,11 +61,6 @@ export const RecursiveLabel = styled(FieldLabel)`
font-size: 13px; font-size: 13px;
`; `;
export const NullableLabel = styled(FieldLabel)`
color: #0e7c86;
font-size: 13px;
`;
export const PatternLabel = styled(FieldLabel)` export const PatternLabel = styled(FieldLabel)`
color: #0e7c86; color: #0e7c86;
&::before, &::before,

View File

@ -1,12 +1,12 @@
import * as React from 'react'; import * as React from 'react';
import { StoreConsumer } from '../components/StoreBuilder'; import { StoreContext } from '../components/StoreBuilder';
import styled, { css } from '../styled-components'; import styled, { css } from '../styled-components';
import { HistoryService } from '../services'; import { HistoryService } from '../services';
// tslint:disable-next-line // tslint:disable-next-line
export const linkifyMixin = className => css` export const linkifyMixin = (className) => css`
${className} { ${className} {
cursor: pointer; cursor: pointer;
margin-left: -20px; margin-left: -20px;
@ -33,36 +33,41 @@ export const linkifyMixin = className => css`
} }
`; `;
const isModifiedEvent = event => const isModifiedEvent = (event) =>
!!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
export class Link extends React.Component<{ to: string; className?: string; children?: any }> { export function Link(props: { to: string; className?: string; children?: any }) {
navigate = (history: HistoryService, event) => { const store = React.useContext(StoreContext);
const clickHandler = React.useCallback(
(event: React.MouseEvent<HTMLAnchorElement>) => {
if (!store) return;
navigate(store.menu.history, event, props.to);
},
[store],
);
if (!store) return null;
return (
<a
className={props.className}
href={store!.menu.history.linkForId(props.to)}
onClick={clickHandler}
aria-label={props.to}
>
{props.children}
</a>
);
}
function navigate(history: HistoryService, event: React.MouseEvent<HTMLAnchorElement>, to: string) {
if ( if (
!event.defaultPrevented && // onClick prevented default !event.defaultPrevented && // onClick prevented default
event.button === 0 && // ignore everything but left clicks event.button === 0 && // ignore everything but left clicks
!isModifiedEvent(event) // ignore clicks with modifier keys !isModifiedEvent(event) // ignore clicks with modifier keys
) { ) {
event.preventDefault(); event.preventDefault();
history.replace(this.props.to); history.replace(to);
}
};
render() {
return (
<StoreConsumer>
{store => (
<a
className={this.props.className}
href={store!.menu.history.linkForId(this.props.to)}
onClick={this.navigate.bind(this, store!.menu.history)}
aria-label={this.props.to}
>
{this.props.children}
</a>
)}
</StoreConsumer>
);
} }
} }

View File

@ -14,6 +14,7 @@ import {
InfoSpanBox, InfoSpanBox,
InfoSpanBoxWrap, InfoSpanBoxWrap,
} from './styled.elements'; } from './styled.elements';
import { l } from '../../services/Labels';
export interface ApiInfoProps { export interface ApiInfoProps {
store: AppStore; store: AppStore;
@ -38,7 +39,7 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
const license = const license =
(info.license && ( (info.license && (
<InfoSpan> <InfoSpan>
License: <a href={info.license.url}>{info.license.name}</a> License: {info.license.identifier ? info.license.identifier : (<a href={info.license.url}>{info.license.name}</a>)}
</InfoSpan> </InfoSpan>
)) || )) ||
null; null;
@ -79,14 +80,14 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
</ApiHeader> </ApiHeader>
{!hideDownloadButton && ( {!hideDownloadButton && (
<p> <p>
Download OpenAPI specification: {l('downloadSpecification')}:
<DownloadButton <DownloadButton
download={downloadFilename || true} download={downloadFilename || true}
target="_blank" target="_blank"
href={downloadLink} href={downloadLink}
onClick={this.handleDownloadClick} onClick={this.handleDownloadClick}
> >
Download {l('download')}
</DownloadButton> </DownloadButton>
</p> </p>
)} )}
@ -100,6 +101,7 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
)) || )) ||
null} null}
</StyledMarkdownBlock> </StyledMarkdownBlock>
<Markdown source={store.spec.info.summary} data-role="redoc-summary"/>
<Markdown source={store.spec.info.description} data-role="redoc-description"/> <Markdown source={store.spec.info.description} data-role="redoc-description"/>
{externalDocs && <ExternalDocumentation externalDocs={externalDocs} />} {externalDocs && <ExternalDocumentation externalDocs={externalDocs} />}
</MiddlePanel> </MiddlePanel>

View File

@ -8,7 +8,7 @@ import { RedocRawOptions } from '../../services/RedocNormalizedOptions';
export interface EnumValuesProps { export interface EnumValuesProps {
values: string[]; values: string[];
type: string; type: string | string[];
} }
export interface EnumValuesState { export interface EnumValuesState {

View File

@ -1,7 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { import {
NullableLabel,
PatternLabel, PatternLabel,
RecursiveLabel, RecursiveLabel,
TypeFormat, TypeFormat,
@ -77,9 +76,22 @@ export class FieldDetails extends React.PureComponent<FieldProps, { patternShown
&gt;{' '} &gt;{' '}
</TypeFormat> </TypeFormat>
)} )}
{schema.contentEncoding && (
<TypeFormat>
{' '}&lt;
{schema.contentEncoding}
&gt;{' '}
</TypeFormat>
)}
{schema.contentMediaType && (
<TypeFormat>
{' '}&lt;
{schema.contentMediaType}
&gt;{' '}
</TypeFormat>
)}
{schema.title && !hideSchemaTitles && <TypeTitle> ({schema.title}) </TypeTitle>} {schema.title && !hideSchemaTitles && <TypeTitle> ({schema.title}) </TypeTitle>}
<ConstraintsView constraints={schema.constraints} /> <ConstraintsView constraints={schema.constraints} />
{schema.nullable && <NullableLabel> {l('nullable')} </NullableLabel>}
{schema.pattern && !hideSchemaPattern && ( {schema.pattern && !hideSchemaPattern && (
<> <>
<PatternLabel> <PatternLabel>
@ -112,6 +124,7 @@ export class FieldDetails extends React.PureComponent<FieldProps, { patternShown
<ExternalDocumentation externalDocs={schema.externalDocs} compact={true} /> <ExternalDocumentation externalDocs={schema.externalDocs} compact={true} />
)} )}
{(renderDiscriminatorSwitch && renderDiscriminatorSwitch(this.props)) || null} {(renderDiscriminatorSwitch && renderDiscriminatorSwitch(this.props)) || null}
{field.const && (<FieldDetail label={l('const') + ':'} value={field.const} />) || null}
</div> </div>
); );
} }

View File

@ -39,7 +39,7 @@ export class Redoc extends React.Component<RedocProps> {
const store = this.props.store; const store = this.props.store;
return ( return (
<ThemeProvider theme={options.theme}> <ThemeProvider theme={options.theme}>
<StoreProvider value={this.props.store}> <StoreProvider value={store}>
<OptionsProvider value={options}> <OptionsProvider value={options}>
<RedocWrap className="redoc-wrap"> <RedocWrap className="redoc-wrap">
<StickyResponsiveSidebar menu={menu} className="menu-content"> <StickyResponsiveSidebar menu={menu} className="menu-content">

View File

@ -1,7 +1,6 @@
import * as PropTypes from 'prop-types';
import * as React from 'react'; import * as React from 'react';
import { RedocNormalizedOptions, RedocRawOptions } from '../services/RedocNormalizedOptions'; import { argValueToBoolean, RedocNormalizedOptions, RedocRawOptions } from '../services/RedocNormalizedOptions';
import { ErrorBoundary } from './ErrorBoundary'; import { ErrorBoundary } from './ErrorBoundary';
import { Loading } from './Loading/Loading'; import { Loading } from './Loading/Loading';
import { Redoc } from './Redoc/Redoc'; import { Redoc } from './Redoc/Redoc';
@ -14,32 +13,9 @@ export interface RedocStandaloneProps {
onLoaded?: (e?: Error) => any; onLoaded?: (e?: Error) => any;
} }
export class RedocStandalone extends React.PureComponent<RedocStandaloneProps> { export const RedocStandalone = function (props: RedocStandaloneProps) {
static propTypes = { const { spec, specUrl, options = {}, onLoaded } = props;
spec: (props, _, componentName) => { const hideLoading = argValueToBoolean(options.hideLoading, false);
if (!props.spec && !props.specUrl) {
return new Error(
`One of props 'spec' or 'specUrl' was not specified in '${componentName}'.`,
);
}
return null;
},
specUrl: (props, _, componentName) => {
if (!props.spec && !props.specUrl) {
return new Error(
`One of props 'spec' or 'specUrl' was not specified in '${componentName}'.`,
);
}
return null;
},
options: PropTypes.any,
onLoaded: PropTypes.any,
};
render() {
const { spec, specUrl, options = {}, onLoaded } = this.props;
const hideLoading = options.hideLoading !== undefined;
const normalizedOpts = new RedocNormalizedOptions(options); const normalizedOpts = new RedocNormalizedOptions(options);
@ -57,4 +33,3 @@ export class RedocStandalone extends React.PureComponent<RedocStandaloneProps> {
</ErrorBoundary> </ErrorBoundary>
); );
} }
}

View File

@ -6,6 +6,7 @@ import { SourceCodeWithCopy } from '../SourceCode/SourceCode';
import { RightPanelHeader, Tab, TabList, TabPanel, Tabs } from '../../common-elements'; import { RightPanelHeader, Tab, TabList, TabPanel, Tabs } from '../../common-elements';
import { OptionsContext } from '../OptionsProvider'; import { OptionsContext } from '../OptionsProvider';
import { l } from '../../services/Labels';
export interface RequestSamplesProps { export interface RequestSamplesProps {
operation: OperationModel; operation: OperationModel;
@ -26,7 +27,7 @@ export class RequestSamples extends React.Component<RequestSamplesProps> {
return ( return (
(hasSamples && ( (hasSamples && (
<div> <div>
<RightPanelHeader> Request samples </RightPanelHeader> <RightPanelHeader> {l('requestSamples')} </RightPanelHeader>
<Tabs defaultIndex={0}> <Tabs defaultIndex={0}>
<TabList hidden={hideTabList}> <TabList hidden={hideTabList}>

View File

@ -5,6 +5,7 @@ import { OperationModel } from '../../services/models';
import { RightPanelHeader, Tab, TabList, TabPanel, Tabs } from '../../common-elements'; import { RightPanelHeader, Tab, TabList, TabPanel, Tabs } from '../../common-elements';
import { PayloadSamples } from '../PayloadSamples/PayloadSamples'; import { PayloadSamples } from '../PayloadSamples/PayloadSamples';
import { l } from '../../services/Labels';
export interface ResponseSamplesProps { export interface ResponseSamplesProps {
operation: OperationModel; operation: OperationModel;
@ -23,7 +24,7 @@ export class ResponseSamples extends React.Component<ResponseSamplesProps> {
return ( return (
(responses.length > 0 && ( (responses.length > 0 && (
<div> <div>
<RightPanelHeader> Response samples </RightPanelHeader> <RightPanelHeader> {l('responseSamples')} </RightPanelHeader>
<Tabs defaultIndex={0}> <Tabs defaultIndex={0}>
<TabList> <TabList>

View File

@ -1,4 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { l } from '../../services/Labels';
import { ResponseModel } from '../../services/models'; import { ResponseModel } from '../../services/models';
import styled from '../../styled-components'; import styled from '../../styled-components';
import { ResponseView } from './Response'; import { ResponseView } from './Response';
@ -26,7 +27,7 @@ export class ResponsesList extends React.PureComponent<ResponseListProps> {
return ( return (
<div> <div>
<ResponsesHeader>{isCallback ? 'Callback responses' : 'Responses'}</ResponsesHeader> <ResponsesHeader>{isCallback ? l('callbackResponses') : l('responses')}</ResponsesHeader>
{responses.map(response => { {responses.map(response => {
return <ResponseView key={response.code} response={response} />; return <ResponseView key={response.code} response={response} />;
})} })}

View File

@ -4,7 +4,8 @@ import { Schema, SchemaProps } from './Schema';
import { ArrayClosingLabel, ArrayOpenningLabel } from '../../common-elements'; import { ArrayClosingLabel, ArrayOpenningLabel } from '../../common-elements';
import styled from '../../styled-components'; import styled from '../../styled-components';
import {humanizeConstraints} from "../../utils"; import { humanizeConstraints } from '../../utils';
import { TypeName } from '../../common-elements/fields';
const PaddedSchema = styled.div` const PaddedSchema = styled.div`
padding-left: ${({ theme }) => theme.spacing.unit * 2}px; padding-left: ${({ theme }) => theme.spacing.unit * 2}px;
@ -13,12 +14,20 @@ const PaddedSchema = styled.div`
export class ArraySchema extends React.PureComponent<SchemaProps> { export class ArraySchema extends React.PureComponent<SchemaProps> {
render() { render() {
const itemsSchema = this.props.schema.items!; const itemsSchema = this.props.schema.items!;
const schema = this.props.schema;
const itemConstraintSchema = ( const itemConstraintSchema = (
min: number | undefined = undefined, min: number | undefined = undefined,
max: number | undefined = undefined, max: number | undefined = undefined,
) => ({ type: 'array', minItems: min, maxItems: max }); ) => ({ type: 'array', minItems: min, maxItems: max });
const minMaxItems = humanizeConstraints(itemConstraintSchema(itemsSchema.schema.minItems, itemsSchema.schema.maxItems)); const minMaxItems = humanizeConstraints(itemConstraintSchema(itemsSchema?.schema?.minItems, itemsSchema?.schema?.maxItems));
if (schema.displayType && !itemsSchema && !minMaxItems.length) {
return (<div>
<TypeName>{schema.displayType}</TypeName>
</div>);
}
return ( return (
<div> <div>

View File

@ -63,13 +63,12 @@ export class Schema extends React.Component<Partial<SchemaProps>> {
return <OneOfSchema schema={schema} {...this.props} />; return <OneOfSchema schema={schema} {...this.props} />;
} }
switch (type) { const types = Array.isArray(type) ? type : [type];
case 'object': if (types.includes('object')) {
if (schema.fields?.length) { if (schema.fields?.length) {
return <ObjectSchema {...(this.props as any)} />; return <ObjectSchema {...(this.props as any)} />;
} }
break; } else if (types.includes('array')) {
case 'array':
return <ArraySchema {...(this.props as any)} />; return <ArraySchema {...(this.props as any)} />;
} }

View File

@ -8,8 +8,8 @@ import { SecurityRequirementModel } from '../../services/models/SecurityRequirem
import { linksCss } from '../Markdown/styled.elements'; import { linksCss } from '../Markdown/styled.elements';
const ScopeName = styled.code` const ScopeName = styled.code`
font-size: ${props => props.theme.typography.code.fontSize}; font-size: ${(props) => props.theme.typography.code.fontSize};
font-family: ${props => props.theme.typography.code.fontFamily}; font-family: ${(props) => props.theme.typography.code.fontFamily};
border: 1px solid ${({ theme }) => theme.colors.border.dark}; border: 1px solid ${({ theme }) => theme.colors.border.dark};
margin: 0 3px; margin: 0 3px;
padding: 0.2em; padding: 0.2em;
@ -67,18 +67,22 @@ export class SecurityRequirement extends React.PureComponent<SecurityRequirement
const security = this.props.security; const security = this.props.security;
return ( return (
<SecurityRequirementOrWrap> <SecurityRequirementOrWrap>
{security.schemes.map(scheme => { {security.schemes.length ? (
security.schemes.map((scheme) => {
return ( return (
<SecurityRequirementAndWrap key={scheme.id}> <SecurityRequirementAndWrap key={scheme.id}>
<Link to={scheme.sectionId}>{scheme.id}</Link> <Link to={scheme.sectionId}>{scheme.id}</Link>
{scheme.scopes.length > 0 && ' ('} {scheme.scopes.length > 0 && ' ('}
{scheme.scopes.map(scope => ( {scheme.scopes.map((scope) => (
<ScopeName key={scope}>{scope}</ScopeName> <ScopeName key={scope}>{scope}</ScopeName>
))} ))}
{scheme.scopes.length > 0 && ') '} {scheme.scopes.length > 0 && ') '}
</SecurityRequirementAndWrap> </SecurityRequirementAndWrap>
); );
})} })
) : (
<SecurityRequirementAndWrap>None</SecurityRequirementAndWrap>
)}
</SecurityRequirementOrWrap> </SecurityRequirementOrWrap>
); );
} }
@ -89,7 +93,7 @@ const AuthHeaderColumn = styled.div`
`; `;
const SecuritiesColumn = styled.div` const SecuritiesColumn = styled.div`
width: ${props => props.theme.schema.defaultDetailsWidth}; width: ${(props) => props.theme.schema.defaultDetailsWidth};
${media.lessThan('small')` ${media.lessThan('small')`
margin-top: 10px; margin-top: 10px;
`} `}

View File

@ -1,5 +1,5 @@
import * as memoize from 'memoize-one/dist/memoize-one.cjs'; // fixme: https://github.com/alexreardon/memoize-one/issues/37 import * as React from 'react';
import { Component, createContext } from 'react'; import { createContext } from 'react';
import { AppStore } from '../services/'; import { AppStore } from '../services/';
import { RedocRawOptions } from '../services/RedocNormalizedOptions'; import { RedocRawOptions } from '../services/RedocNormalizedOptions';
@ -14,7 +14,7 @@ export interface StoreBuilderProps {
onLoaded?: (e?: Error) => void; onLoaded?: (e?: Error) => void;
children: (props: { loading: boolean; store?: AppStore }) => any; children: (props: { loading: boolean; store: AppStore | null }) => any;
} }
export interface StoreBuilderState { export interface StoreBuilderState {
@ -25,79 +25,47 @@ export interface StoreBuilderState {
prevSpecUrl?: string; prevSpecUrl?: string;
} }
const { Provider, Consumer } = createContext<AppStore | undefined>(undefined); const StoreContext = createContext<AppStore | undefined>(undefined);
export { Provider as StoreProvider, Consumer as StoreConsumer }; const { Provider, Consumer } = StoreContext;
export { Provider as StoreProvider, Consumer as StoreConsumer, StoreContext };
export class StoreBuilder extends Component<StoreBuilderProps, StoreBuilderState> { export function StoreBuilder(props: StoreBuilderProps) {
static getDerivedStateFromProps(nextProps: StoreBuilderProps, prevState: StoreBuilderState) { const {spec, specUrl, options, onLoaded, children } = props;
if (nextProps.specUrl !== prevState.prevSpecUrl || nextProps.spec !== prevState.prevSpec) {
return {
loading: true,
resolvedSpec: null,
prevSpec: nextProps.spec,
prevSpecUrl: nextProps.specUrl,
};
}
return null; const [resolvedSpec, setResolvedSpec] = React.useState<any>(null);
}
state: StoreBuilderState = { React.useEffect(() => {
loading: true, async function load() {
resolvedSpec: null, if (!spec && !specUrl) {
};
@memoize
makeStore(spec, specUrl, options) {
if (!spec) {
return undefined; return undefined;
} }
setResolvedSpec(null);
const resolved = await loadAndBundleSpec(spec || specUrl!);
setResolvedSpec(resolved);
}
load();
}, [spec, specUrl])
const store = React.useMemo(() => {
if (!resolvedSpec) return null;
try { try {
return new AppStore(spec, specUrl, options); return new AppStore(resolvedSpec, specUrl, options);
} catch (e) { } catch (e) {
if (this.props.onLoaded) { if (onLoaded) {
this.props.onLoaded(e); onLoaded(e);
} }
throw e; throw e;
} }
} }, [resolvedSpec, specUrl, options]);
componentDidMount() { React.useEffect(() => {
this.load(); if (store && onLoaded) {
onLoaded();
} }
}, [store, onLoaded])
componentDidUpdate() { return children({
if (this.state.resolvedSpec === null) { loading: !store,
this.load(); store,
} else if (!this.state.loading && this.props.onLoaded) {
// may run multiple time
this.props.onLoaded();
}
}
async load() {
const { specUrl, spec } = this.props;
try {
const resolvedSpec = await loadAndBundleSpec(spec || specUrl!);
this.setState({ resolvedSpec, loading: false });
} catch (e) {
if (this.props.onLoaded) {
this.props.onLoaded(e);
}
this.setState({ error: e });
}
}
render() {
if (this.state.error) {
throw this.state.error;
}
const { specUrl, options } = this.props;
const { loading, resolvedSpec } = this.state;
return this.props.children({
loading,
store: this.makeStore(resolvedSpec, specUrl, options),
}); });
} }
}

View File

@ -0,0 +1,27 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import { OpenAPIParser } from '../../services';
import { SecurityRequirementModel } from '../../services/models/SecurityRequirement';
import { SecurityRequirement } from '../SecurityRequirement/SecurityRequirement';
import { RedocNormalizedOptions } from '../../services/RedocNormalizedOptions';
const options = new RedocNormalizedOptions({});
describe('Components', () => {
describe('SecurityRequirement', () => {
describe('SecurityRequirement', () => {
it('should render \'None\' when empty object in security open api', () => {
const parser = new OpenAPIParser({ openapi: '3.0', info: { title: 'test', version: '0' }, paths: {} },
undefined,
options,
);
const securityRequirement = new SecurityRequirementModel({}, parser);
const securityElement = shallow(
<SecurityRequirement key={1} security={securityRequirement} />
).getElement();
expect(securityElement.props.children.type.target).toEqual('span');
expect(securityElement.props.children.props.children).toEqual('None');
});
});
});
});

View File

@ -6,6 +6,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
<Field <Field
field={ field={
FieldModel { FieldModel {
"const": "",
"deprecated": false, "deprecated": false,
"description": "", "description": "",
"example": undefined, "example": undefined,
@ -17,7 +18,10 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"required": false, "required": false,
"schema": SchemaModel { "schema": SchemaModel {
"activeOneOf": 0, "activeOneOf": 0,
"const": "",
"constraints": Array [], "constraints": Array [],
"contentEncoding": undefined,
"contentMediaType": undefined,
"default": undefined, "default": undefined,
"deprecated": false, "deprecated": false,
"description": "", "description": "",
@ -29,7 +33,6 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"format": undefined, "format": undefined,
"isCircular": undefined, "isCircular": undefined,
"isPrimitive": true, "isPrimitive": true,
"nullable": false,
"options": "<<<filtered>>>", "options": "<<<filtered>>>",
"pattern": undefined, "pattern": undefined,
"pointer": "#/components/schemas/Dog/properties/packSize", "pointer": "#/components/schemas/Dog/properties/packSize",
@ -56,6 +59,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
<Field <Field
field={ field={
FieldModel { FieldModel {
"const": "",
"deprecated": false, "deprecated": false,
"description": "", "description": "",
"example": undefined, "example": undefined,
@ -67,7 +71,10 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"required": true, "required": true,
"schema": SchemaModel { "schema": SchemaModel {
"activeOneOf": 0, "activeOneOf": 0,
"const": "",
"constraints": Array [], "constraints": Array [],
"contentEncoding": undefined,
"contentMediaType": undefined,
"default": undefined, "default": undefined,
"deprecated": false, "deprecated": false,
"description": "", "description": "",
@ -79,7 +86,6 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"format": undefined, "format": undefined,
"isCircular": undefined, "isCircular": undefined,
"isPrimitive": true, "isPrimitive": true,
"nullable": false,
"options": "<<<filtered>>>", "options": "<<<filtered>>>",
"pattern": undefined, "pattern": undefined,
"pointer": "#/components/schemas/Dog/properties/type", "pointer": "#/components/schemas/Dog/properties/type",

View File

@ -6,9 +6,9 @@ export {
Section, Section,
StyledDropdown, StyledDropdown,
SimpleDropdown, SimpleDropdown,
DropdownOption,
} from './common-elements/'; } from './common-elements/';
export { OpenAPIEncoding } from './types'; export type { DropdownOption } from './common-elements';
export type { OpenAPIEncoding } from './types';
export * from './services'; export * from './services';
export * from './utils'; export * from './utils';

View File

@ -1,15 +1,2 @@
import 'core-js/es/promise';
import 'core-js/es/array/find';
import 'core-js/es/array/includes';
import 'core-js/es/object/assign';
import 'core-js/es/object/entries';
import 'core-js/es/object/is';
import 'core-js/es/string/ends-with';
import 'core-js/es/string/starts-with';
import 'core-js/es/map';
import 'core-js/es/symbol';
import 'unfetch/polyfill/index'; import 'unfetch/polyfill/index';
import 'url-polyfill'; import 'core-js/es/symbol';

View File

@ -145,7 +145,10 @@ export class AppStore {
if (idx === -1 && IS_BROWSER) { if (idx === -1 && IS_BROWSER) {
const $description = document.querySelector('[data-role="redoc-description"]'); const $description = document.querySelector('[data-role="redoc-description"]');
const $summary = document.querySelector('[data-role="redoc-summary"]');
if ($description) elements.push($description); if ($description) elements.push($description);
if ($summary) elements.push($summary);
} }
this.marker.addOnly(elements); this.marker.addOnly(elements);

View File

@ -6,10 +6,16 @@ export interface LabelsConfig {
deprecated: string; deprecated: string;
example: string; example: string;
examples: string; examples: string;
nullable: string;
recursive: string; recursive: string;
arrayOf: string; arrayOf: string;
webhook: string; webhook: string;
const: string;
download: string;
downloadSpecification: string;
responses: string;
callbackResponses: string;
requestSamples: string;
responseSamples: string;
} }
export type LabelsConfigRaw = Partial<LabelsConfig>; export type LabelsConfigRaw = Partial<LabelsConfig>;
@ -22,10 +28,16 @@ const labels: LabelsConfig = {
deprecated: 'Deprecated', deprecated: 'Deprecated',
example: 'Example', example: 'Example',
examples: 'Examples', examples: 'Examples',
nullable: 'Nullable',
recursive: 'Recursive', recursive: 'Recursive',
arrayOf: 'Array of ', arrayOf: 'Array of ',
webhook: 'Event', webhook: 'Event',
const: 'Value',
download: 'Download',
downloadSpecification: 'Download OpenAPI specification',
responses: 'Responses',
callbackResponses: 'Callback responses',
requestSamples: 'Request samples',
responseSamples: 'Response samples',
}; };
export function setRedocLabels(_labels?: LabelsConfigRaw) { export function setRedocLabels(_labels?: LabelsConfigRaw) {

View File

@ -53,7 +53,7 @@ export class MenuBuilder {
const spec = parser.spec; const spec = parser.spec;
const items: ContentItemModel[] = []; const items: ContentItemModel[] = [];
const tagsMap = MenuBuilder.getTagsWithOperations(spec); const tagsMap = MenuBuilder.getTagsWithOperations(parser, spec);
items.push(...MenuBuilder.addMarkdownItems(spec.info.description || '', undefined, 1, options)); items.push(...MenuBuilder.addMarkdownItems(spec.info.description || '', undefined, 1, options));
if (spec['x-tagGroups'] && spec['x-tagGroups'].length > 0) { if (spec['x-tagGroups'] && spec['x-tagGroups'].length > 0) {
items.push( items.push(
@ -215,24 +215,33 @@ export class MenuBuilder {
/** /**
* collects tags and maps each tag to list of operations belonging to this tag * collects tags and maps each tag to list of operations belonging to this tag
*/ */
static getTagsWithOperations(spec: OpenAPISpec): TagsInfoMap { static getTagsWithOperations(parser: OpenAPIParser, spec: OpenAPISpec): TagsInfoMap {
const tags: TagsInfoMap = {}; const tags: TagsInfoMap = {};
const webhooks = spec['x-webhooks'] || spec.webhooks;
for (const tag of spec.tags || []) { for (const tag of spec.tags || []) {
tags[tag.name] = { ...tag, operations: [] }; tags[tag.name] = { ...tag, operations: [] };
} }
getTags(spec.paths); if (webhooks) {
if (spec['x-webhooks']) { getTags(parser, webhooks, true);
getTags(spec['x-webhooks'], true);
} }
function getTags(paths: OpenAPIPaths, isWebhook?: boolean) { if (spec.paths){
getTags(parser, spec.paths);
}
function getTags(parser: OpenAPIParser, paths: OpenAPIPaths, isWebhook?: boolean) {
for (const pathName of Object.keys(paths)) { for (const pathName of Object.keys(paths)) {
const path = paths[pathName]; const path = paths[pathName];
const operations = Object.keys(path).filter(isOperationName); const operations = Object.keys(path).filter(isOperationName);
for (const operationName of operations) { for (const operationName of operations) {
const operationInfo = path[operationName]; const operationInfo = path[operationName];
let operationTags = operationInfo.tags; if (path.$ref) {
const resolvedPaths = parser.deref<OpenAPIPaths>(path as OpenAPIPaths);
getTags(parser, { [pathName]: resolvedPaths }, isWebhook);
continue;
}
let operationTags = operationInfo?.tags;
if (!operationTags || !operationTags.length) { if (!operationTags || !operationTags.length) {
// empty tag // empty tag

View File

@ -45,9 +45,9 @@ class RefCounter {
export class OpenAPIParser { export class OpenAPIParser {
specUrl?: string; specUrl?: string;
spec: OpenAPISpec; spec: OpenAPISpec;
mergeRefs: Set<string>;
private _refCounter: RefCounter = new RefCounter(); private _refCounter: RefCounter = new RefCounter();
private allowMergeRefs: boolean = false;
constructor( constructor(
spec: OpenAPISpec, spec: OpenAPISpec,
@ -58,8 +58,7 @@ export class OpenAPIParser {
this.preprocess(spec); this.preprocess(spec);
this.spec = spec; this.spec = spec;
this.allowMergeRefs = spec.openapi.startsWith('3.1');
this.mergeRefs = new Set();
const href = IS_BROWSER ? window.location.href : ''; const href = IS_BROWSER ? window.location.href : '';
if (typeof specUrl === 'string') { if (typeof specUrl === 'string') {
@ -149,7 +148,7 @@ export class OpenAPIParser {
* @param obj object to dereference * @param obj object to dereference
* @param forceCircular whether to dereference even if it is circular ref * @param forceCircular whether to dereference even if it is circular ref
*/ */
deref<T extends object>(obj: OpenAPIRef | T, forceCircular = false): T { deref<T extends object>(obj: OpenAPIRef | T, forceCircular = false, mergeAsAllOf = false): T {
if (this.isRef(obj)) { if (this.isRef(obj)) {
const schemaName = getDefinitionName(obj.$ref); const schemaName = getDefinitionName(obj.$ref);
if (schemaName && this.options.ignoreNamedSchemas.has(schemaName)) { if (schemaName && this.options.ignoreNamedSchemas.has(schemaName)) {
@ -165,16 +164,36 @@ export class OpenAPIParser {
return Object.assign({}, resolved, { 'x-circular-ref': true }); return Object.assign({}, resolved, { 'x-circular-ref': true });
} }
// deref again in case one more $ref is here // deref again in case one more $ref is here
let result = resolved;
if (this.isRef(resolved)) { if (this.isRef(resolved)) {
const res = this.deref(resolved); result = this.deref(resolved, false, mergeAsAllOf);
this.exitRef(resolved); this.exitRef(resolved);
return res;
} }
return resolved; return this.allowMergeRefs ? this.mergeRefs(obj, resolved, mergeAsAllOf) : result;
} }
return obj; return obj;
} }
mergeRefs(ref, resolved, mergeAsAllOf: boolean) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { $ref, ...rest } = ref;
const keys = Object.keys(rest);
if (keys.length === 0) {
return resolved;
}
if (mergeAsAllOf && keys.some((k) => k !== 'description' && k !== 'title' && k !== 'externalDocs')) {
return {
allOf: [resolved, rest],
};
} else {
// small optimization
return {
...resolved,
...rest,
};
}
}
shalowDeref<T extends object>(obj: OpenAPIRef | T): T { shalowDeref<T extends object>(obj: OpenAPIRef | T): T {
if (this.isRef(obj)) { if (this.isRef(obj)) {
return this.byRef<T>(obj.$ref)!; return this.byRef<T>(obj.$ref)!;
@ -225,7 +244,7 @@ export class OpenAPIParser {
return undefined; return undefined;
} }
const resolved = this.deref(subSchema, forceCircular); const resolved = this.deref(subSchema, forceCircular, true);
const subRef = subSchema.$ref || undefined; const subRef = subSchema.$ref || undefined;
const subMerged = this.mergeAllOf(resolved, subRef, forceCircular, used$Refs); const subMerged = this.mergeAllOf(resolved, subRef, forceCircular, used$Refs);
receiver.parentRefs!.push(...(subMerged.parentRefs || [])); receiver.parentRefs!.push(...(subMerged.parentRefs || []));
@ -234,7 +253,7 @@ export class OpenAPIParser {
schema: subMerged, schema: subMerged,
}; };
}) })
.filter(child => child !== undefined) as Array<{ .filter((child) => child !== undefined) as Array<{
$ref: string | undefined; $ref: string | undefined;
schema: MergedOpenAPISchema; schema: MergedOpenAPISchema;
}>; }>;
@ -265,7 +284,7 @@ export class OpenAPIParser {
{ allOf: [receiver.properties[prop], subSchema.properties[prop]] }, { allOf: [receiver.properties[prop], subSchema.properties[prop]] },
$ref + '/properties/' + prop, $ref + '/properties/' + prop,
); );
receiver.properties[prop] = mergedProp receiver.properties[prop] = mergedProp;
this.exitParents(mergedProp); // every prop resolution should have separate recursive stack this.exitParents(mergedProp); // every prop resolution should have separate recursive stack
} }
} }
@ -313,7 +332,7 @@ export class OpenAPIParser {
const def = this.deref(schemas[defName]); const def = this.deref(schemas[defName]);
if ( if (
def.allOf !== undefined && def.allOf !== undefined &&
def.allOf.find(obj => obj.$ref !== undefined && $refs.indexOf(obj.$ref) > -1) def.allOf.find((obj) => obj.$ref !== undefined && $refs.indexOf(obj.$ref) > -1)
) { ) {
res['#/components/schemas/' + defName] = [def['x-discriminator-value'] || defName]; res['#/components/schemas/' + defName] = [def['x-discriminator-value'] || defName];
} }
@ -339,7 +358,7 @@ export class OpenAPIParser {
const beforeAllOf = allOf.slice(0, i); const beforeAllOf = allOf.slice(0, i);
const afterAllOf = allOf.slice(i + 1); const afterAllOf = allOf.slice(i + 1);
return { return {
oneOf: sub.oneOf.map(part => { oneOf: sub.oneOf.map((part) => {
const merged = this.mergeAllOf({ const merged = this.mergeAllOf({
allOf: [...beforeAllOf, part, ...afterAllOf], allOf: [...beforeAllOf, part, ...afterAllOf],
}); });

View File

@ -44,7 +44,7 @@ export interface RedocRawOptions {
hideSchemaPattern?: boolean; hideSchemaPattern?: boolean;
} }
function argValueToBoolean(val?: string | boolean, defaultValue?: boolean): boolean { export function argValueToBoolean(val?: string | boolean, defaultValue?: boolean): boolean {
if (val === undefined) { if (val === undefined) {
return defaultValue || false; return defaultValue || false;
} }

View File

@ -1,4 +1,4 @@
import { OpenAPIExternalDocumentation, OpenAPISpec } from '../types'; import { OpenAPIExternalDocumentation, OpenAPIPath, OpenAPISpec, Referenced } from '../types';
import { ContentItemModel, MenuBuilder } from './MenuBuilder'; import { ContentItemModel, MenuBuilder } from './MenuBuilder';
import { ApiInfoModel } from './models/ApiInfo'; import { ApiInfoModel } from './models/ApiInfo';
@ -28,6 +28,7 @@ export class SpecStore {
this.externalDocs = this.parser.spec.externalDocs; this.externalDocs = this.parser.spec.externalDocs;
this.contentItems = MenuBuilder.buildStructure(this.parser, this.options); this.contentItems = MenuBuilder.buildStructure(this.parser, this.options);
this.securitySchemes = new SecuritySchemesModel(this.parser); this.securitySchemes = new SecuritySchemesModel(this.parser);
this.webhooks = new WebhookModel(this.parser, options, this.parser.spec['x-webhooks']); const webhookPath: Referenced<OpenAPIPath> = {...this.parser?.spec?.['x-webhooks'], ...this.parser?.spec.webhooks};
this.webhooks = new WebhookModel(this.parser, options, webhookPath);
} }
} }

View File

@ -0,0 +1,66 @@
{
"openapi": "3.1.0",
"info": {
"version": "1.0.0",
"title": "Swagger Petstore"
},
"webhooks": {
"myWebhook": {
"$ref": "#/components/pathItems/catsWebhook",
"description": "Overriding description",
"summary": "Overriding summary"
}
},
"components": {
"pathItems": {
"catsWebhook": {
"put": {
"summary": "Get a cat details after update",
"description": "Get a cat details after update",
"operationId": "updatedCat",
"tags": [
"pet"
],
"requestBody": {
"description": "Information about cat in the system",
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/Pet"
}
}
}
},
"responses": {
"200": {
"description": "update Cat details"
}
}
},
"post": {
"summary": "Create new cat",
"description": "Info about new cat",
"operationId": "createdCat",
"tags": [
"pet"
],
"requestBody": {
"description": "Information about cat in the system",
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/Pet"
}
}
}
},
"responses": {
"200": {
"description": "create Cat details"
}
}
}
}
}
}
}

View File

@ -34,5 +34,33 @@ describe('Models', () => {
const info = new ApiInfoModel(parser); const info = new ApiInfoModel(parser);
expect(info.description).toEqual('Test description\nsome text\n'); expect(info.description).toEqual('Test description\nsome text\n');
}); });
test('should correctly populate summary up to the first md heading', () => {
parser.spec = {
openapi: '3.1.0',
info: {
summary: 'Test summary\nsome text\n## Heading\n test',
},
} as any;
const info = new ApiInfoModel(parser);
expect(info.summary).toEqual('Test summary\nsome text\n## Heading\n test');
});
test('should correctly populate license identifier', () => {
parser.spec = {
openapi: '3.1.0',
info: {
license: {
name: 'MIT',
identifier: 'MIT',
url: 'https://opensource.org/licenses/MIT'
}
},
} as any;
const { license = { identifier: null } } = new ApiInfoModel(parser);
expect(license.identifier).toEqual('MIT');
});
}); });
}); });

View File

@ -0,0 +1,25 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import { MenuBuilder } from '../../MenuBuilder';
import { OpenAPIParser } from '../../OpenAPIParser';
import { RedocNormalizedOptions } from '../../RedocNormalizedOptions';
const opts = new RedocNormalizedOptions({});
describe('Models', () => {
describe('MenuBuilder', () => {
let parser;
test('should resolve pathItems', () => {
const spec = require('../fixtures/3.1/pathItems.json');
parser = new OpenAPIParser(spec, undefined, opts);
const contentItems = MenuBuilder.buildStructure(parser, opts);
expect(contentItems).toHaveLength(1);
expect(contentItems[0].items).toHaveLength(2);
expect(contentItems[0].id).toEqual('tag/pet');
expect(contentItems[0].name).toEqual('pet');
expect(contentItems[0].type).toEqual('tag');
});
});
});

View File

@ -7,6 +7,7 @@ export class ApiInfoModel implements OpenAPIInfo {
version: string; version: string;
description: string; description: string;
summary: string;
termsOfService?: string; termsOfService?: string;
contact?: OpenAPIContact; contact?: OpenAPIContact;
license?: OpenAPILicense; license?: OpenAPILicense;
@ -17,6 +18,7 @@ export class ApiInfoModel implements OpenAPIInfo {
constructor(private parser: OpenAPIParser) { constructor(private parser: OpenAPIParser) {
Object.assign(this, parser.spec.info); Object.assign(this, parser.spec.info);
this.description = parser.spec.info.description || ''; this.description = parser.spec.info.description || '';
this.summary = parser.spec.info.summary || '';
const firstHeadingLinePos = this.description.search(/^##?\s+/m); const firstHeadingLinePos = this.description.search(/^##?\s+/m);
if (firstHeadingLinePos > -1) { if (firstHeadingLinePos > -1) {

View File

@ -55,6 +55,7 @@ export class FieldModel {
extensions?: Record<string, any>; extensions?: Record<string, any>;
explode: boolean; explode: boolean;
style?: OpenAPIParameterStyle; style?: OpenAPIParameterStyle;
const?: any;
serializationMime?: string; serializationMime?: string;
@ -111,6 +112,8 @@ export class FieldModel {
if (options.showExtensions) { if (options.showExtensions) {
this.extensions = extractExtensions(info, options.showExtensions); this.extensions = extractExtensions(info, options.showExtensions);
} }
this.const = this.schema?.const || info?.const || '';
} }
@action @action

View File

@ -58,7 +58,7 @@ export class MediaTypeModel {
if (this.schema && this.schema.oneOf) { if (this.schema && this.schema.oneOf) {
this.examples = {}; this.examples = {};
for (const subSchema of this.schema.oneOf) { for (const subSchema of this.schema.oneOf) {
const sample = Sampler.sample(subSchema.rawSchema, samplerOptions, parser.spec); const sample = Sampler.sample(subSchema.rawSchema as any, samplerOptions, parser.spec);
if (this.schema.discriminatorProp && typeof sample === 'object' && sample) { if (this.schema.discriminatorProp && typeof sample === 'object' && sample) {
sample[this.schema.discriminatorProp] = subSchema.title; sample[this.schema.discriminatorProp] = subSchema.title;
@ -78,7 +78,7 @@ export class MediaTypeModel {
default: new ExampleModel( default: new ExampleModel(
parser, parser,
{ {
value: Sampler.sample(info.schema, samplerOptions, parser.spec), value: Sampler.sample(info.schema as any, samplerOptions, parser.spec),
}, },
this.name, this.name,
info.encoding, info.encoding,

View File

@ -25,7 +25,7 @@ import { l } from '../Labels';
export class SchemaModel { export class SchemaModel {
pointer: string; pointer: string;
type: string; type: string | string[];
displayType: string; displayType: string;
typePrefix: string = ''; typePrefix: string = '';
title: string; title: string;
@ -60,6 +60,9 @@ export class SchemaModel {
rawSchema: OpenAPISchema; rawSchema: OpenAPISchema;
schema: MergedOpenAPISchema; schema: MergedOpenAPISchema;
extensions?: Record<string, any>; extensions?: Record<string, any>;
const: any;
contentEncoding?: string;
contentMediaType?: string;
/** /**
* @param isChild if schema discriminator Child * @param isChild if schema discriminator Child
@ -75,7 +78,7 @@ export class SchemaModel {
makeObservable(this); makeObservable(this);
this.pointer = schemaOrRef.$ref || pointer || ''; this.pointer = schemaOrRef.$ref || pointer || '';
this.rawSchema = parser.deref(schemaOrRef); this.rawSchema = parser.deref(schemaOrRef, false, true);
this.schema = parser.mergeAllOf(this.rawSchema, this.pointer, isChild); this.schema = parser.mergeAllOf(this.rawSchema, this.pointer, isChild);
this.init(parser, isChild); this.init(parser, isChild);
@ -97,6 +100,10 @@ export class SchemaModel {
this.activeOneOf = idx; this.activeOneOf = idx;
} }
hasType(type: string) {
return this.type === type || (Array.isArray(this.type) && this.type.includes(type));
}
init(parser: OpenAPIParser, isChild: boolean) { init(parser: OpenAPIParser, isChild: boolean) {
const schema = this.schema; const schema = this.schema;
this.isCircular = schema['x-circular-ref']; this.isCircular = schema['x-circular-ref'];
@ -106,7 +113,6 @@ export class SchemaModel {
this.description = schema.description || ''; this.description = schema.description || '';
this.type = schema.type || detectType(schema); this.type = schema.type || detectType(schema);
this.format = schema.format; this.format = schema.format;
this.nullable = !!schema.nullable;
this.enum = schema.enum || []; this.enum = schema.enum || [];
this.example = schema.example; this.example = schema.example;
this.deprecated = !!schema.deprecated; this.deprecated = !!schema.deprecated;
@ -114,12 +120,26 @@ export class SchemaModel {
this.externalDocs = schema.externalDocs; this.externalDocs = schema.externalDocs;
this.constraints = humanizeConstraints(schema); this.constraints = humanizeConstraints(schema);
this.displayType = this.type;
this.displayFormat = this.format; this.displayFormat = this.format;
this.isPrimitive = isPrimitiveType(schema, this.type); this.isPrimitive = isPrimitiveType(schema, this.type);
this.default = schema.default; this.default = schema.default;
this.readOnly = !!schema.readOnly; this.readOnly = !!schema.readOnly;
this.writeOnly = !!schema.writeOnly; this.writeOnly = !!schema.writeOnly;
this.const = schema.const || '';
this.contentEncoding = schema.contentEncoding;
this.contentMediaType = schema.contentMediaType;
if (!!schema.nullable || schema['x-nullable']) {
if (Array.isArray(this.type) && !this.type.some((value) => value === null || value === 'null')) {
this.type = [...this.type, 'null'];
} else if (!Array.isArray(this.type) && (this.type !== null || this.type !== 'null')) {
this.type = [this.type, 'null'];
}
}
this.displayType = Array.isArray(this.type)
? this.type.map(item => item === null ? 'null' : item).join(' or ')
: this.type;
if (this.isCircular) { if (this.isCircular) {
return; return;
@ -154,9 +174,9 @@ export class SchemaModel {
return; return;
} }
if (this.type === 'object') { if (this.hasType('object')) {
this.fields = buildFields(parser, schema, this.pointer, this.options); this.fields = buildFields(parser, schema, this.pointer, this.options);
} else if (this.type === 'array' && schema.items) { } else if (this.hasType('array') && schema.items) {
this.items = new SchemaModel(parser, schema.items, this.pointer + '/items', this.options); this.items = new SchemaModel(parser, schema.items, this.pointer + '/items', this.options);
this.displayType = pluralizeType(this.items.displayType); this.displayType = pluralizeType(this.items.displayType);
this.displayFormat = this.items.format; this.displayFormat = this.items.format;
@ -169,6 +189,11 @@ export class SchemaModel {
if (this.items.isPrimitive) { if (this.items.isPrimitive) {
this.enum = this.items.enum; this.enum = this.items.enum;
} }
if (Array.isArray(this.type)) {
const filteredType = this.type.filter(item => item !== 'array');
if (filteredType.length)
this.displayType += ` or ${filteredType.join(' or ')}`;
}
} }
if (this.enum.length && this.options.sortEnumValuesAlphabetically) { if (this.enum.length && this.options.sortEnumValuesAlphabetically) {
@ -178,7 +203,7 @@ export class SchemaModel {
private initOneOf(oneOf: OpenAPISchema[], parser: OpenAPIParser) { private initOneOf(oneOf: OpenAPISchema[], parser: OpenAPIParser) {
this.oneOf = oneOf!.map((variant, idx) => { this.oneOf = oneOf!.map((variant, idx) => {
const derefVariant = parser.deref(variant); const derefVariant = parser.deref(variant, false, true);
const merged = parser.mergeAllOf(derefVariant, this.pointer + '/oneOf/' + idx); const merged = parser.mergeAllOf(derefVariant, this.pointer + '/oneOf/' + idx);
@ -186,7 +211,7 @@ export class SchemaModel {
const title = const title =
isNamedDefinition(variant.$ref) && !merged.title isNamedDefinition(variant.$ref) && !merged.title
? JsonPointer.baseName(variant.$ref) ? JsonPointer.baseName(variant.$ref)
: merged.title; : `${(merged.title || '')}${(merged.const && JSON.stringify(merged.const)) || ''}`;
const schema = new SchemaModel( const schema = new SchemaModel(
parser, parser,

View File

@ -1,8 +1,8 @@
import { OpenAPIPath, Referenced } from '../../types'; import { OpenAPIPath, Referenced } from '../../types';
import { OpenAPIParser } from '../OpenAPIParser'; import { OpenAPIParser } from '../OpenAPIParser';
import { OperationModel } from './Operation'; import { OperationModel } from './Operation';
import { isOperationName } from '../..';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions'; import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
import { isOperationName } from '../..';
export class WebhookModel { export class WebhookModel {
operations: OperationModel[] = []; operations: OperationModel[] = [];
@ -14,12 +14,21 @@ export class WebhookModel {
) { ) {
const webhooks = parser.deref<OpenAPIPath>(infoOrRef || {}); const webhooks = parser.deref<OpenAPIPath>(infoOrRef || {});
parser.exitRef(infoOrRef); parser.exitRef(infoOrRef);
this.initWebhooks(parser, webhooks, options);
}
initWebhooks(parser: OpenAPIParser, webhooks: OpenAPIPath, options: RedocNormalizedOptions) {
for (const webhookName of Object.keys(webhooks)) { for (const webhookName of Object.keys(webhooks)) {
const webhook = webhooks[webhookName]; const webhook = webhooks[webhookName];
const operations = Object.keys(webhook).filter(isOperationName); const operations = Object.keys(webhook).filter(isOperationName);
for (const operationName of operations) { for (const operationName of operations) {
const operationInfo = webhook[operationName]; const operationInfo = webhook[operationName];
if (webhook.$ref) {
const resolvedWebhook = parser.deref<OpenAPIPath>(webhook || {});
this.initWebhooks(parser, { [operationName]: resolvedWebhook }, options);
}
if (!operationInfo) continue;
const operation = new OperationModel( const operation = new OperationModel(
parser, parser,
{ {

View File

@ -1,5 +1,6 @@
import * as Enzyme from 'enzyme'; import * as Enzyme from 'enzyme';
import * as Adapter from 'enzyme-adapter-react-16'; import * as Adapter from '@wojtekmaj/enzyme-adapter-react-17';
import 'raf/polyfill'; import 'raf/polyfill';
Enzyme.configure({ adapter: new Adapter() }); Enzyme.configure({ adapter: new Adapter() });

View File

@ -1,8 +1,8 @@
import * as styledComponents from 'styled-components'; import * as styledComponents from 'styled-components';
import { ResolvedThemeInterface } from './theme'; import type { ResolvedThemeInterface } from './theme';
export { ResolvedThemeInterface }; export type { ResolvedThemeInterface };
const { const {
default: styled, default: styled,
@ -10,7 +10,7 @@ const {
createGlobalStyle, createGlobalStyle,
keyframes, keyframes,
ThemeProvider, ThemeProvider,
} = styledComponents as styledComponents.ThemedStyledComponentsModule<ResolvedThemeInterface>; } = styledComponents as unknown as styledComponents.ThemedStyledComponentsModule<ResolvedThemeInterface>;
export const media = { export const media = {
lessThan(breakpoint, print?: boolean, extra?: string) { lessThan(breakpoint, print?: boolean, extra?: string) {

View File

@ -10,6 +10,7 @@ export interface OpenAPISpec {
tags?: OpenAPITag[]; tags?: OpenAPITag[];
externalDocs?: OpenAPIExternalDocumentation; externalDocs?: OpenAPIExternalDocumentation;
'x-webhooks'?: OpenAPIPaths; 'x-webhooks'?: OpenAPIPaths;
webhooks?: OpenAPIPaths;
} }
export interface OpenAPIInfo { export interface OpenAPIInfo {
@ -17,6 +18,7 @@ export interface OpenAPIInfo {
version: string; version: string;
description?: string; description?: string;
summary?: string;
termsOfService?: string; termsOfService?: string;
contact?: OpenAPIContact; contact?: OpenAPIContact;
license?: OpenAPILicense; license?: OpenAPILicense;
@ -56,6 +58,7 @@ export interface OpenAPIPath {
trace?: OpenAPIOperation; trace?: OpenAPIOperation;
servers?: OpenAPIServer[]; servers?: OpenAPIServer[];
parameters?: Array<Referenced<OpenAPIParameter>>; parameters?: Array<Referenced<OpenAPIParameter>>;
$ref?: string;
} }
export interface OpenAPIXCodeSample { export interface OpenAPIXCodeSample {
@ -96,6 +99,7 @@ export interface OpenAPIParameter {
examples?: { [media: string]: Referenced<OpenAPIExample> }; examples?: { [media: string]: Referenced<OpenAPIExample> };
content?: { [media: string]: OpenAPIMediaType }; content?: { [media: string]: OpenAPIMediaType };
encoding?: Record<string, OpenAPIEncoding>; encoding?: Record<string, OpenAPIEncoding>;
const?: any;
} }
export interface OpenAPIExample { export interface OpenAPIExample {
@ -107,7 +111,7 @@ export interface OpenAPIExample {
export interface OpenAPISchema { export interface OpenAPISchema {
$ref?: string; $ref?: string;
type?: string; type?: string | string[];
properties?: { [name: string]: OpenAPISchema }; properties?: { [name: string]: OpenAPISchema };
additionalProperties?: boolean | OpenAPISchema; additionalProperties?: boolean | OpenAPISchema;
description?: string; description?: string;
@ -129,9 +133,9 @@ export interface OpenAPISchema {
title?: string; title?: string;
multipleOf?: number; multipleOf?: number;
maximum?: number; maximum?: number;
exclusiveMaximum?: boolean; exclusiveMaximum?: boolean | number;
minimum?: number; minimum?: number;
exclusiveMinimum?: boolean; exclusiveMinimum?: boolean | number;
maxLength?: number; maxLength?: number;
minLength?: number; minLength?: number;
pattern?: string; pattern?: string;
@ -142,6 +146,9 @@ export interface OpenAPISchema {
minProperties?: number; minProperties?: number;
enum?: any[]; enum?: any[];
example?: any; example?: any;
const?: string;
contentEncoding?: string;
contentMediaType?: string;
} }
export interface OpenAPIDiscriminator { export interface OpenAPIDiscriminator {
@ -271,4 +278,5 @@ export interface OpenAPIContact {
export interface OpenAPILicense { export interface OpenAPILicense {
name: string; name: string;
url?: string; url?: string;
identifier?: string;
} }

View File

@ -1,17 +1,23 @@
import * as yaml from 'yaml-js'; import * as yaml from 'js-yaml';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { resolve } from 'path'; import { resolve } from 'path';
import { loadAndBundleSpec } from '../loadAndBundleSpec'; import { loadAndBundleSpec } from '../loadAndBundleSpec';
describe('#loadAndBundleSpec', () => { describe('#loadAndBundleSpec', () => {
it('should load And Bundle Spec demo/openapi.yaml', async () => { it('should load And Bundle Spec demo/openapi.yaml', async () => {
const spec = yaml.load(readFileSync(resolve(__dirname, '../../../demo/openapi.yaml'))); const spec = yaml.load(readFileSync(resolve(__dirname, '../../../demo/openapi.yaml'), 'utf-8'));
const bundledSpec = await loadAndBundleSpec(spec);
expect(bundledSpec).toMatchSnapshot();
});
it('should load And Bundle Spec demo/openapi-3-1.yaml', async () => {
const spec = yaml.load(readFileSync(resolve(__dirname, '../../../demo/openapi-3-1.yaml'), 'utf-8'));
const bundledSpec = await loadAndBundleSpec(spec); const bundledSpec = await loadAndBundleSpec(spec);
expect(bundledSpec).toMatchSnapshot(); expect(bundledSpec).toMatchSnapshot();
}); });
it('should load And Bundle Spec demo/swagger.yaml', async () => { it('should load And Bundle Spec demo/swagger.yaml', async () => {
const spec = yaml.load(readFileSync(resolve(__dirname, '../../../demo/swagger.yaml'))); const spec = yaml.load(readFileSync(resolve(__dirname, '../../../demo/swagger.yaml'), 'utf-8'));
const bundledSpec = await loadAndBundleSpec(spec); const bundledSpec = await loadAndBundleSpec(spec);
expect(bundledSpec).toMatchSnapshot(); expect(bundledSpec).toMatchSnapshot();
}); });

View File

@ -101,6 +101,13 @@ describe('Utils', () => {
expect(getOperationSummary(operation as any).length).toBe(50); expect(getOperationSummary(operation as any).length).toBe(50);
}); });
it('Should return pathName if no summary, operationId, description', () => {
const operation = {
pathName: '/sandbox/test'
};
expect(getOperationSummary(operation as any)).toBe('/sandbox/test');
});
it('Should return <no summary> if no info', () => { it('Should return <no summary> if no info', () => {
const operation = { const operation = {
description: undefined, description: undefined,
@ -167,6 +174,79 @@ describe('Utils', () => {
expect(isPrimitiveType(schema)).toEqual(false); expect(isPrimitiveType(schema)).toEqual(false);
}); });
it('should return true for array contains object and schema hasn\'t properties', () => {
const schema = {
type: ['object', 'string'],
};
expect(isPrimitiveType(schema)).toEqual(true);
});
it('should return false for array contains object and schema has properties', () => {
const schema = {
type: ['object', 'string'],
properties: {
a: {
type: 'string',
},
},
};
expect(isPrimitiveType(schema)).toEqual(false);
});
it('should return false for array contains array type and schema has items', () => {
const schema = {
type: ['array'],
items: {
type: 'object',
additionalProperties: true,
},
};
expect(isPrimitiveType(schema)).toEqual(false);
});
it('should return false for array contains object and array types and schema has items', () => {
const schema = {
type: ['array', 'object'],
items: {
type: 'object',
additionalProperties: true,
},
};
expect(isPrimitiveType(schema)).toEqual(false);
});
it('should return false for array contains object and array types and schema has properties', () => {
const schema = {
type: ['array', 'object'],
properties: {
a: {
type: 'string',
},
},
};
expect(isPrimitiveType(schema)).toEqual(false);
});
it('should return true for array contains array of strings', () => {
const schema = {
type: 'array',
items: {
type: 'array',
items: {
type: 'string'
},
},
};
expect(isPrimitiveType(schema)).toEqual(true);
});
it('Should return false for array of string which include the null value', () => {
const schema = {
type: ['object', 'string', 'null'],
};
expect(isPrimitiveType(schema)).toEqual(true);
});
it('Should return false for array with non-empty objects', () => { it('Should return false for array with non-empty objects', () => {
const schema = { const schema = {
type: 'array', type: 'array',

View File

@ -19,6 +19,7 @@ import 'prismjs/components/prism-ruby.js';
import 'prismjs/components/prism-scala.js'; import 'prismjs/components/prism-scala.js';
import 'prismjs/components/prism-sql.js'; import 'prismjs/components/prism-sql.js';
import 'prismjs/components/prism-swift.js'; import 'prismjs/components/prism-swift.js';
import 'prismjs/components/prism-yaml.js';
const DEFAULT_LANG = 'clike'; const DEFAULT_LANG = 'clike';

View File

@ -1,4 +1,10 @@
import { Source, Document, bundle, Config } from '@redocly/openapi-core'; import type { Source, Document } from '@redocly/openapi-core';
// eslint-disable-next-line import/no-internal-modules
import { bundle } from '@redocly/openapi-core/lib/bundle';
// eslint-disable-next-line import/no-internal-modules
import { Config } from '@redocly/openapi-core/lib/config/config';
/* tslint:disable-next-line:no-implicit-dependencies */ /* tslint:disable-next-line:no-implicit-dependencies */
import { convertObj } from 'swagger2openapi'; import { convertObj } from 'swagger2openapi';
import { OpenAPISpec } from '../types'; import { OpenAPISpec } from '../types';

View File

@ -1,12 +1,12 @@
import { dirname } from 'path'; import { dirname } from 'path';
import * as URLtemplate from 'url-template'; import * as URLtemplate from 'url-template';
import { ExtendedOpenAPIOperation } from '../services';
import { FieldModel } from '../services/models'; import { FieldModel } from '../services/models';
import { OpenAPIParser } from '../services/OpenAPIParser'; import { OpenAPIParser } from '../services/OpenAPIParser';
import { import {
OpenAPIEncoding, OpenAPIEncoding,
OpenAPIMediaType, OpenAPIMediaType,
OpenAPIOperation,
OpenAPIParameter, OpenAPIParameter,
OpenAPIParameterStyle, OpenAPIParameterStyle,
OpenAPISchema, OpenAPISchema,
@ -56,17 +56,19 @@ const operationNames = {
patch: true, patch: true,
delete: true, delete: true,
options: true, options: true,
$ref: true,
}; };
export function isOperationName(key: string): boolean { export function isOperationName(key: string): boolean {
return key in operationNames; return key in operationNames;
} }
export function getOperationSummary(operation: OpenAPIOperation): string { export function getOperationSummary(operation: ExtendedOpenAPIOperation): string {
return ( return (
operation.summary || operation.summary ||
operation.operationId || operation.operationId ||
(operation.description && operation.description.substring(0, 50)) || (operation.description && operation.description.substring(0, 50)) ||
operation.pathName ||
'<no summary>' '<no summary>'
); );
} }
@ -81,6 +83,8 @@ const schemaKeywordTypes = {
maxLength: 'string', maxLength: 'string',
minLength: 'string', minLength: 'string',
pattern: 'string', pattern: 'string',
contentEncoding: 'string',
contentMediaType: 'string',
items: 'array', items: 'array',
maxItems: 'array', maxItems: 'array',
@ -95,7 +99,7 @@ const schemaKeywordTypes = {
}; };
export function detectType(schema: OpenAPISchema): string { export function detectType(schema: OpenAPISchema): string {
if (schema.type !== undefined) { if (schema.type !== undefined && !Array.isArray(schema.type)) {
return schema.type; return schema.type;
} }
const keywords = Object.keys(schemaKeywordTypes); const keywords = Object.keys(schemaKeywordTypes);
@ -109,25 +113,25 @@ export function detectType(schema: OpenAPISchema): string {
return 'any'; return 'any';
} }
export function isPrimitiveType(schema: OpenAPISchema, type: string | undefined = schema.type) { export function isPrimitiveType(schema: OpenAPISchema, type: string | string[] | undefined = schema.type) {
if (schema.oneOf !== undefined || schema.anyOf !== undefined) { if (schema.oneOf !== undefined || schema.anyOf !== undefined) {
return false; return false;
} }
if (type === 'object') { let isPrimitive = true;
return schema.properties !== undefined const isArray = Array.isArray(type);
if (type === 'object' || (isArray && type?.includes('object'))) {
isPrimitive = schema.properties !== undefined
? Object.keys(schema.properties).length === 0 ? Object.keys(schema.properties).length === 0
: schema.additionalProperties === undefined; : schema.additionalProperties === undefined;
} }
if (type === 'array') { if (schema.items !== undefined && (type === 'array' || (isArray && type?.includes('array')))) {
if (schema.items === undefined) { isPrimitive = isPrimitiveType(schema.items, schema.items.type);
return true;
}
return false;
} }
return true; return isPrimitive;
} }
export function isJsonLike(contentType: string): boolean { export function isJsonLike(contentType: string): boolean {
@ -366,12 +370,12 @@ export function langFromMime(contentType: string): string {
} }
export function isNamedDefinition(pointer?: string): boolean { export function isNamedDefinition(pointer?: string): boolean {
return /^#\/components\/schemas\/[^\/]+$/.test(pointer || ''); return /^#\/components\/(schemas|pathItems)\/[^\/]+$/.test(pointer || '');
} }
export function getDefinitionName(pointer?: string): string | undefined { export function getDefinitionName(pointer?: string): string | undefined {
if (!pointer) return undefined; if (!pointer) return undefined;
const match = pointer.match(/^#\/components\/schemas\/([^\/]+)$/); const match = pointer.match(/^#\/components\/(schemas|pathItems)\/([^\/]+)$/);
return match === null ? undefined : match[1] return match === null ? undefined : match[1]
} }
@ -444,6 +448,18 @@ export function humanizeConstraints(schema: OpenAPISchema): string[] {
numberRange += schema.minimum; numberRange += schema.minimum;
} }
if (typeof schema.exclusiveMinimum === 'number' || typeof schema.exclusiveMaximum === 'number') {
let minimum = 0;
let maximum = 0;
if (schema.minimum) minimum = schema.minimum;
if (typeof schema.exclusiveMinimum === 'number') minimum = minimum <= schema.exclusiveMinimum ? minimum : schema.exclusiveMinimum;
if (schema.maximum) maximum = schema.maximum;
if (typeof schema.exclusiveMaximum === 'number') maximum = maximum > schema.exclusiveMaximum ? maximum : schema.exclusiveMaximum;
numberRange = `[${minimum} .. ${maximum}]`
}
if (numberRange !== undefined) { if (numberRange !== undefined) {
res.push(numberRange); res.push(numberRange);
} }

View File

@ -2,6 +2,7 @@
import ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); import ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
import * as webpack from 'webpack'; import * as webpack from 'webpack';
import * as path from 'path'; import * as path from 'path';
import { getBabelLoader, webpackIgnore } from './config/webpack-utils';
const nodeExternals = require('webpack-node-externals')({ const nodeExternals = require('webpack-node-externals')({
// bundle in modules that need transpiling + non-js (e.g. css) // bundle in modules that need transpiling + non-js (e.g. css)
@ -31,7 +32,7 @@ const BANNER = `ReDoc - OpenAPI/Swagger-generated API Reference Documentation
Version: ${VERSION} Version: ${VERSION}
Repo: https://github.com/Redocly/redoc`; Repo: https://github.com/Redocly/redoc`;
export default (env: { standalone?: boolean } = {}, { mode }) => ({ export default (env: { standalone?: boolean } = {}) => ({
entry: env.standalone ? ['./src/polyfills.ts', './src/standalone.tsx'] : './src/index.ts', entry: env.standalone ? ['./src/polyfills.ts', './src/standalone.tsx'] : './src/index.ts',
output: { output: {
filename: env.standalone ? 'redoc.standalone.js' : 'redoc.lib.js', filename: env.standalone ? 'redoc.standalone.js' : 'redoc.lib.js',
@ -42,18 +43,20 @@ export default (env: { standalone?: boolean } = {}, { mode }) => ({
}, },
devtool: 'source-map', devtool: 'source-map',
resolve: { resolve: {
extensions: ['.ts', '.tsx', '.js', '.json'], extensions: ['.ts', '.tsx', '.js', '.mjs', '.json'],
}, fallback: {
node: { path: require.resolve('path-browserify'),
fs: 'empty', http: false,
fs: false,
os: false,
}
}, },
performance: false, performance: false,
optimization: { // target: 'node',
minimize: !!env.standalone, externalsPresets: env.standalone ? {} : { node: true },
},
externals: env.standalone externals: env.standalone
? { ? {
esprima: 'esprima', esprima: 'null',
'node-fetch': 'null', 'node-fetch': 'null',
'node-fetch-h2': 'null', 'node-fetch-h2': 'null',
yaml: 'null', yaml: 'null',
@ -61,7 +64,7 @@ export default (env: { standalone?: boolean } = {}, { mode }) => ({
} }
: (context, request, callback) => { : (context, request, callback) => {
// ignore node-fetch dep of swagger2openapi as it is not used // ignore node-fetch dep of swagger2openapi as it is not used
if (/esprima|node-fetch|node-fetch-h2|yaml|safe-json-stringify$/i.test(request)) { if (/esprima|node-fetch|node-fetch-h2|\/yaml|safe-json-stringify$/i.test(request)) {
return callback(null, 'var undefined'); return callback(null, 'var undefined');
} }
return nodeExternals(context, request, callback); return nodeExternals(context, request, callback);
@ -70,51 +73,22 @@ export default (env: { standalone?: boolean } = {}, { mode }) => ({
module: { module: {
rules: [ rules: [
{ {
test: /\.tsx?$/, test: /\.(tsx?|[cm]?js)$/,
use: [ use: [getBabelLoader({useBuiltIns: !!env.standalone})],
{ exclude: {
loader: 'ts-loader', and: [/node_modules/],
options: { not: {
compilerOptions: { or: [
module: 'es2015', /swagger2openapi/,
declaration: false, /reftools/,
}, /openapi-sampler/,
}, /mobx/,
}, /oas-resolver/,
{ /oas-kit-common/,
loader: 'babel-loader', /oas-schema-walker/,
options: { /\@redocly\/openapi-core/,
generatorOpts: { /colorette/,
decoratorsBeforeExport: true,
},
plugins: [
['@babel/plugin-syntax-typescript', { isTSX: true }],
['@babel/plugin-syntax-decorators', { legacy: true }],
'@babel/plugin-syntax-jsx',
[
'babel-plugin-styled-components',
{
minify: true,
displayName: mode !== 'production',
},
], ],
],
},
},
],
exclude: [/node_modules/],
},
{
test: /node_modules\/(swagger2openapi|reftools|oas-resolver|oas-kit-common|oas-schema-walker)\/.*\.js$/,
use: {
loader: 'ts-loader',
options: {
instance: 'ts2js-transpiler-only',
transpileOnly: true,
compilerOptions: {
allowJs: true,
declaration: false,
},
}, },
}, },
}, },
@ -127,22 +101,19 @@ export default (env: { standalone?: boolean } = {}, { mode }) => ({
}, },
}, },
}, },
{ enforce: 'pre', test: /\.js$/, loader: 'source-map-loader' },
], ],
}, },
plugins: [ plugins: [
new webpack.DefinePlugin({ new webpack.DefinePlugin({
__REDOC_VERSION__: VERSION, __REDOC_VERSION__: VERSION,
__REDOC_REVISION__: REVISION, __REDOC_REVISION__: REVISION,
'process.env': '{}',
'process.platform': '"browser"',
'process.stdout': 'null',
}), }),
new ForkTsCheckerWebpackPlugin({ logger: { infrastructure: 'silent', issues: 'console' } }), new ForkTsCheckerWebpackPlugin({ logger: { infrastructure: 'silent', issues: 'console' } }),
new webpack.BannerPlugin(BANNER), new webpack.BannerPlugin(BANNER),
ignore(/js-yaml\/dumper\.js$/), webpackIgnore(/js-yaml\/dumper\.js$/),
ignore(/json-schema-ref-parser\/lib\/dereference\.js/), env.standalone ? webpackIgnore(/^\.\/SearchWorker\.worker$/) : undefined,
env.standalone ? ignore(/^\.\/SearchWorker\.worker$/) : ignore(/$non-existing^/), ].filter(Boolean),
],
}); });
function ignore(regexp) {
return new webpack.NormalModuleReplacementPlugin(regexp, require.resolve('lodash/noop.js'));
}