merge master

This commit is contained in:
Oprysk 2022-01-05 11:10:49 +02:00
commit 12a2ce9b6d
84 changed files with 5632 additions and 6430 deletions

View File

@ -2,51 +2,56 @@ name: Publish cli
on:
push:
tags:
- v[0-9]*.[0-9]*.[0-9]*
branches:
- master
jobs:
bundle:
needs: [check-version-cli]
if: needs.check-version-cli.outputs.changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- name: Cache node modules
uses: actions/cache@v2
with:
path: ~/.npm # npm cache files are stored in `~/.npm` on Linux/macOS
key: npm-${{ hashFiles('package-lock.json') }}
restore-keys: |
npm-${{ hashFiles('package-lock.json') }}
npm-
- run: npm ci
- run: npm run bundle
- name: Store bundle artifact
uses: actions/upload-artifact@v2
with:
name: bundles-cli
path: bundles
retention-days: 1
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- name: Cache node modules
uses: actions/cache@v2
with:
path: ~/.npm # npm cache files are stored in `~/.npm` on Linux/macOS
key: npm-${{ hashFiles('package-lock.json') }}
restore-keys: |
npm-${{ hashFiles('package-lock.json') }}
npm-
- run: npm ci
- run: npm run bundle
- name: Store bundle artifact
uses: actions/upload-artifact@v2
with:
name: bundles-cli
path: bundles
retention-days: 1
unit-tests:
needs: [check-version-cli]
if: needs.check-version-cli.outputs.changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- run: npm ci
- run: npm test
- uses: actions/checkout@v1
- run: npm ci
- run: npm test
e2e-tests:
needs: [bundle]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- run: npm ci
- name: Download bundled artifact
uses: actions/download-artifact@v2
with:
name: bundles
path: bundles-cli
- run: npm run e2e
- uses: actions/checkout@v1
- run: npm ci
- name: Download bundled artifact
uses: actions/download-artifact@v2
with:
name: bundles-cli
path: bundles
- run: npm run e2e
bundle-cli:
needs: [check-version-cli]
if: needs.check-version-cli.outputs.changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
@ -72,7 +77,6 @@ jobs:
check-version-cli:
name: Check Version
runs-on: ubuntu-latest
needs: [bundle-cli, unit-tests, e2e-tests]
outputs:
changed: ${{ steps.check.outputs.changed }}
steps:
@ -88,13 +92,14 @@ jobs:
file-url: https://unpkg.com/redoc-cli/package.json
static-checking: localIsNew
publish-cli:
needs: [ check-version-cli ]
needs: [bundle-cli, unit-tests, e2e-tests]
if: needs.check-version-cli.outputs.changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v1
with:
node-version: "14.x"
node-version: '14.x'
registry-url: 'https://registry.npmjs.org'
- uses: actions/checkout@v2
- name: Download cli bundled artifact
uses: actions/download-artifact@v2

View File

@ -9,73 +9,75 @@ jobs:
bundle:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- name: Cache node modules
uses: actions/cache@v2
with:
path: ~/.npm # npm cache files are stored in `~/.npm` on Linux/macOS
key: npm-${{ hashFiles('package-lock.json') }}
restore-keys: |
npm-${{ hashFiles('package-lock.json') }}
npm-
- run: npm ci
- run: npm run bundle
- name: Store bundle artifact
uses: actions/upload-artifact@v2
with:
name: bundles
path: bundles
retention-days: 1
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- name: Cache node modules
uses: actions/cache@v2
with:
path: ~/.npm # npm cache files are stored in `~/.npm` on Linux/macOS
key: npm-${{ hashFiles('package-lock.json') }}
restore-keys: |
npm-${{ hashFiles('package-lock.json') }}
npm-
- run: npm ci
- run: npm run bundle
- name: Store bundle artifact
uses: actions/upload-artifact@v2
with:
name: bundles
path: bundles
retention-days: 1
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- run: npm ci
- run: npm test
- uses: actions/checkout@v1
- run: npm ci
- run: npm test
e2e-tests:
needs: [bundle]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- run: npm ci
- name: Download bundled artifact
uses: actions/download-artifact@v2
with:
name: bundles
path: bundles
- run: npm run e2e
deploy-demo:
needs: [bundle, unit-tests, e2e-tests]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Install dependencies
run: npm ci
- name: Download bundled artifacts
- run: npm ci
- name: Download bundled artifact
uses: actions/download-artifact@v2
with:
name: bundles
path: bundles
- name: Build package
run: npm run build:demo
- name: Deploy to S3 bucket
run: npm run deploy:demo
- name: Invalidate
run: aws cloudfront create-invalidation --distribution-id ${{ secrets.CF_DEMO_DISTRIBUTION_ID }} --paths "/*"
- run: npm run e2e
# disable this for now
# deploy-demo:
# needs: [bundle, unit-tests, e2e-tests]
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v1
# - name: Configure AWS Credentials
# uses: aws-actions/configure-aws-credentials@v1
# with:
# aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
# aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
# aws-region: us-east-1
# - name: Install dependencies
# run: npm ci
# - name: Download bundled artifacts
# uses: actions/download-artifact@v2
# with:
# name: bundles
# path: bundles
# - name: Build package
# run: npm run build:demo
# - name: Deploy to S3 bucket
# run: npm run deploy:demo
# - name: Invalidate
# run: aws cloudfront create-invalidation --distribution-id ${{ secrets.CF_DEMO_DISTRIBUTION_ID }} --paths "/*"
publish:
needs: [bundle, unit-tests, e2e-tests]
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v1
with:
node-version: "14.x"
node-version: '14.x'
registry-url: 'https://registry.npmjs.org'
- uses: actions/checkout@v2
- name: Download bundled artifacts
uses: actions/download-artifact@v2
@ -91,10 +93,8 @@ jobs:
npm-${{ hashFiles('package-lock.json') }}
npm-
- name: Before deploy
run: npm run declarations
run: npm ci && npm run declarations
- name: Publish to NPM
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: After script
run: cat ./coverage/lcov.info | coveralls

4
.husky/pre-commit Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run pre-commit

1
.prettierignore Normal file
View File

@ -0,0 +1 @@
*.md

View File

@ -1,3 +1,51 @@
# [2.0.0-rc.59](https://github.com/Redocly/redoc/compare/v2.0.0-rc.58...v2.0.0-rc.59) (2021-12-09)
### Bug Fixes
* fix scroll in example dropdown ([#1803](https://github.com/Redocly/redoc/issues/1803)) ([bc2d9a7](https://github.com/Redocly/redoc/commit/bc2d9a7d9cd530274483fecd136db290a5b46ff7))
* x-examples for request body param does not display [#1743](https://github.com/Redocly/redoc/issues/1743) ([#1826](https://github.com/Redocly/redoc/issues/1826)) ([aaa3b32](https://github.com/Redocly/redoc/commit/aaa3b3280c8422d450e8849ae02135dde199d6d5))
### Features
* add option sideNavStyle ([#1805](https://github.com/Redocly/redoc/pull/1805)) ([2e4663b](https://github.com/Redocly/redoc/commit/2e4663b3b7022f25d3dc808afbcb3b3ad9483c41))
# [2.0.0-rc.58](https://github.com/Redocly/redoc/compare/v2.0.0-rc.57...v2.0.0-rc.58) (2021-11-29)
### Bug Fixes
* add browser build for webpack 5 ([#1796](https://github.com/Redocly/redoc/issues/1796)) ([0e43ad3](https://github.com/Redocly/redoc/commit/0e43ad3102cfba8c4b30e59500ad4efc53f01c2d))
* Default boolean property value not rendered [#1779](https://github.com/Redocly/redoc/issues/1779) ([#1781](https://github.com/Redocly/redoc/issues/1781)) ([734080c](https://github.com/Redocly/redoc/commit/734080c35471d16f87004f7f9a51dcdeee1278a6))
* exclusiveMin/Max shows incorect range ([#1799](https://github.com/Redocly/redoc/issues/1799)) ([b604bd8](https://github.com/Redocly/redoc/commit/b604bd8da874f07e9e9f8b193ad10117a5f5059c))
* mobile view in docker image ([#1795](https://github.com/Redocly/redoc/issues/1795)) ([ad652b9](https://github.com/Redocly/redoc/commit/ad652b9c7fbcd84a6e83397272de64e57213fe9a))
# [2.0.0-rc.57](https://github.com/Redocly/redoc/compare/v2.0.0-rc.56...v2.0.0-rc.57) (2021-10-11)
### Bug Fixes
* fix deref logic for oas3.1 ([#1767](https://github.com/Redocly/redoc/issues/1767)) ([4fb9c83](https://github.com/Redocly/redoc/commit/4fb9c835256b9e44bcecabde7baf0f0f3e5beb3f))
* improve publish action scripts ([#1729](https://github.com/Redocly/redoc/issues/1729)) ([952c05c](https://github.com/Redocly/redoc/commit/952c05c6b4b95fe6082611fed9e2f0913272b904))
* No match scenario in search ([#1667](https://github.com/Redocly/redoc/issues/1667)) ([352a851](https://github.com/Redocly/redoc/commit/352a8518576dfb6b240ec41212a64f1c7312ab67))
* OpenAPI 3.1: Missing description when $ref used [#1727](https://github.com/Redocly/redoc/issues/1727) ([fe6909e](https://github.com/Redocly/redoc/commit/fe6909ed80dd6053b48c30f63a2460614bf957a9))
* OpenAPI 3.1: Missing description when $ref used [#1727](https://github.com/Redocly/redoc/issues/1727) ([35f7787](https://github.com/Redocly/redoc/commit/35f77878de7d1dd250040771f17757a5a6ce85f9))
* Redoc spelling ([c87600d](https://github.com/Redocly/redoc/commit/c87600d520f037d291169b44b5803a35af16b5a5))
* Schema for events incorrectly omits readOnly and includes writeOnly ([#1720](https://github.com/Redocly/redoc/issues/1720) [#1540](https://github.com/Redocly/redoc/issues/1540)) ([a8e0c29](https://github.com/Redocly/redoc/commit/a8e0c296852661dec1dcad2388d7589f9e0d3609))
* scrolling to the first item ([#1753](https://github.com/Redocly/redoc/issues/1753)) ([bccd213](https://github.com/Redocly/redoc/commit/bccd21394ef79940c2efbe24a0d866c7af103d94))
* The number of items in the array in the array is incorrect [#1762](https://github.com/Redocly/redoc/issues/1762) ([#1763](https://github.com/Redocly/redoc/issues/1763)) ([3b8d644](https://github.com/Redocly/redoc/commit/3b8d6441bd9978b849a53021d40fd4fe150272ea))
### Features
* add q/kdb+ syntax highlighting ([#1605](https://github.com/Redocly/redoc/issues/1605)) ([43451ba](https://github.com/Redocly/redoc/commit/43451ba4cd24270b8629a967d3fd2ce2eed8912e))
* new option generatedPayloadSamplesMaxDepth ([#1642](https://github.com/Redocly/redoc/issues/1642)) ([bd9390a](https://github.com/Redocly/redoc/commit/bd9390a5bfc5458c06121110db33968a20fcebe4))
# [2.0.0-rc.56](https://github.com/Redocly/redoc/compare/v2.0.0-rc.53...v2.0.0-rc.56) (2021-08-11)
@ -8,7 +56,7 @@
* 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 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))

View File

@ -3,19 +3,19 @@
# Generate interactive API documentation from OpenAPI definitions
[![Build Status](https://travis-ci.com/Redocly/redoc.svg?branch=master)](https://travis-ci.com/Redocly/redoc) [![Coverage Status](https://coveralls.io/repos/Redocly/redoc/badge.svg?branch=master&service=github)](https://coveralls.io/github/Redocly/redoc?branch=master) [![dependencies Status](https://david-dm.org/Redocly/redoc/status.svg)](https://david-dm.org/Redocly/redoc) [![devDependencies Status](https://david-dm.org/Redocly/redoc/dev-status.svg)](https://david-dm.org/Redocly/redoc#info=devDependencies) [![npm](http://img.shields.io/npm/v/redoc.svg)](https://www.npmjs.com/package/redoc) [![License](https://img.shields.io/npm/l/redoc.svg)](https://github.com/Redocly/redoc/blob/master/LICENSE)
[![Build Status](https://travis-ci.com/Redocly/redoc.svg?branch=master)](https://travis-ci.com/Redocly/redoc) [![Coverage Status](https://coveralls.io/repos/Redocly/redoc/badge.svg?branch=master&service=github)](https://coveralls.io/github/Redocly/redoc?branch=master) [![npm](http://img.shields.io/npm/v/redoc.svg)](https://www.npmjs.com/package/redoc) [![License](https://img.shields.io/npm/l/redoc.svg)](https://github.com/Redocly/redoc/blob/master/LICENSE)
[![bundle size](http://img.badgesize.io/https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js?compression=gzip&max=300000)](https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js) [![npm](https://img.shields.io/npm/dm/redoc.svg)](https://www.npmjs.com/package/redoc) [![](https://data.jsdelivr.com/v1/package/npm/redoc/badge)](https://www.jsdelivr.com/package/npm/redoc) [![Docker Build Status](https://img.shields.io/docker/build/redocly/redoc.svg)](https://hub.docker.com/r/redocly/redoc/)
</div>
**This is the README for the `2.x` version of Redoc (React-based).**
**This is the README for the `2.x` version of Redoc (React-based).**
**The README for the `1.x` version is on the [v1.x](https://github.com/Redocly/redoc/tree/v1.x) branch**
## About Redoc
Redoc is an open-source tool for generating documentation from OpenAPI (fka Swagger) definitions.
Redoc is an open-source tool for generating documentation from OpenAPI (fka Swagger) definitions.
By default Redoc offers a three-panel, responsive layout:
By default Redoc offers a three-panel, responsive layout:
- The left panel contains a search bar and navigation menu.
- The central panel contains the documentation.
@ -133,7 +133,7 @@ Additionally, all the 1.x releases are hosted on our GitHub Pages-based CDN **(d
Redocly's OpenAPI CLI is an open source command-line tool that you can use to lint
your OpenAPI definition. Linting helps you to catch errors and inconsistencies in your
OpenAPI definition before publishing.
OpenAPI definition before publishing.
Refer to [Lint configuration](https://redoc.ly/docs/cli/guides/lint/) in the OpenAPI documentation for more information.
@ -173,16 +173,17 @@ replace the `spec-url` attribute with the url or local file address to your defi
```
For step-by-step instructions for how to get started using Redoc
to render your OpenAPI definition, refer to the
to render your OpenAPI definition, refer to the
[**Redoc quickstart guide**](https://redoc.ly/docs/redoc/quickstart/intro/).
See [**IE11 Support Notes**](docs/usage-with-ie11.md) for information on
See [**IE11 Support Notes**](docs/usage-with-ie11.md) for information on
IE support for Redoc.
## Redoc CLI
For more information on Redoc's commmand-line interface, refer to
[**Using the Redoc CLI**](https://redoc.ly/docs/redoc/quickstart/cli/).
## Configuration
### Security Definition location
@ -239,6 +240,9 @@ You can use all of the following options with the standalone version of the <red
* `payloadSampleIdx` - if set, payload sample will be inserted at this index or last. Indexes start from 0.
* `theme` - ReDoc theme. For details check [theme docs](#redoc-theme-object).
* `untrustedSpec` - if set, the spec is considered untrusted and all HTML/markdown is sanitized to prevent XSS. **Disabled by default** for performance reasons. **Enable this option if you work with untrusted user data!**
* `sideNavStyle` - can be specified in various ways:
* **summary-only**: displays a summary in the sidebar navigation item. (**default**)
* **path-only**: displays a path in the sidebar navigation item.
### `<redoc>` theme object
* `spacing`

View File

@ -61,6 +61,12 @@ YargsParser.command(
type: 'boolean',
});
yargs.option('h', {
alias: 'host',
type: 'string',
default: '127.0.0.1',
});
yargs.option('p', {
alias: 'port',
type: 'number',
@ -93,7 +99,7 @@ YargsParser.command(
};
try {
await serve(argv.port as number, argv.spec as string, config);
await serve(argv.host as string, argv.port as number, argv.spec as string, config);
} catch (e) {
handleError(e);
}
@ -167,7 +173,7 @@ YargsParser.command(
describe: 'ReDoc options, use dot notation, e.g. options.nativeScrollbars',
}).argv;
async function serve(port: number, pathToSpec: string, options: Options = {}) {
async function serve(host: string, port: number, pathToSpec: string, options: Options = {}) {
let spec = await loadAndBundleSpec(isURL(pathToSpec) ? pathToSpec : resolve(pathToSpec));
let pageHTML = await getPageHTML(spec, pathToSpec, options);
const server = createServer((request, response) => {
@ -201,7 +207,7 @@ async function serve(port: number, pathToSpec: string, options: Options = {}) {
console.log();
server.listen(port, () => console.log(`Server started: http://127.0.0.1:${port}`));
server.listen(port, host, () => console.log(`Server started: http://${host}:${port}`));
if (options.watch && existsSync(pathToSpec)) {
const pathToSpecDirectory = resolve(dirname(pathToSpec));

141
cli/npm-shrinkwrap.json generated
View File

@ -1,12 +1,12 @@
{
"name": "redoc-cli",
"version": "0.12.3",
"version": "0.13.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "redoc-cli",
"version": "0.12.3",
"version": "0.13.2",
"license": "MIT",
"dependencies": {
"chokidar": "^3.5.1",
@ -17,7 +17,7 @@
"node-libs-browser": "^2.2.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"redoc": "2.0.0-rc.56",
"redoc": "2.0.0-rc.59",
"styled-components": "^5.3.0",
"yargs": "^17.0.1"
},
@ -198,13 +198,13 @@
"integrity": "sha512-GoXw0U2Qaa33m3eUcxuHnHpNvHjNlLo0gtV091XBpaRINaB4X6FGCG5XKxSFNFiPpugUDqNruHzaqpTdDm4AOg=="
},
"node_modules/@redocly/ajv": {
"version": "6.12.4",
"resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-6.12.4.tgz",
"integrity": "sha512-RB6vWO78v6c+SW/3bZh+XZMr4nGdJKAiPGsBALuUZnLuCiQ7aXCT1AuFHqnfS2gyXbEUEj+kw8p4ux8KdAfs3A==",
"version": "8.6.2",
"resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.6.2.tgz",
"integrity": "sha512-tU8fQs0D76ZKhJ2cWtnfQthWqiZgGBx0gH0+5D8JvaBEBaqA8foPPBt3Nonwr3ygyv5xrw2IzKWgIY86BlGs+w==",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
},
"funding": {
@ -213,11 +213,11 @@
}
},
"node_modules/@redocly/openapi-core": {
"version": "1.0.0-beta.50",
"resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.0.0-beta.50.tgz",
"integrity": "sha512-GuXn4IETxpbRd8dlAQDQPtvqOpbMvPMeC/e5mv5MOXkLIznNk4vjiQVe6QSCbZbCHzzpb2+89B6S7asebPm4Rg==",
"version": "1.0.0-beta.62",
"resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.0.0-beta.62.tgz",
"integrity": "sha512-g4iPB7qpSNFCOpf/FgKiic+QCaCn4mdNQOWVEPuwpN7l72hlQ7J3YUa9cssJomkJXXxZ1zdP4h208s12LkhwVA==",
"dependencies": {
"@redocly/ajv": "^6.12.3",
"@redocly/ajv": "^8.6.2",
"@types/node": "^14.11.8",
"colorette": "^1.2.0",
"js-levenshtein": "^1.1.6",
@ -232,9 +232,9 @@
}
},
"node_modules/@redocly/openapi-core/node_modules/@types/node": {
"version": "14.17.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.3.tgz",
"integrity": "sha512-e6ZowgGJmTuXa3GyaPbTGxX17tnThl2aSSizrFthQ7m9uLGZBXiGhgE55cjRZTF5kjZvYn9EOPOMljdjwbflxw=="
"version": "14.17.21",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.21.tgz",
"integrity": "sha512-zv8ukKci1mrILYiQOwGSV4FpkZhyxQtuFWGya2GujWg+zVAeRQ4qbaMmWp9vb9889CFA8JECH7lkwCL6Ygg8kA=="
},
"node_modules/@redocly/react-dropdown-aria": {
"version": "2.0.12",
@ -283,7 +283,8 @@
"node_modules/@types/node": {
"version": "15.12.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz",
"integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww=="
"integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==",
"dev": true
},
"node_modules/ansi-regex": {
"version": "5.0.0",
@ -637,9 +638,9 @@
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"node_modules/colorette": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
"integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w=="
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
"integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="
},
"node_modules/concat-map": {
"version": "0.0.1",
@ -893,11 +894,6 @@
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
},
"node_modules/fast-safe-stringify": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz",
@ -925,9 +921,7 @@
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"os": ["darwin"],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
@ -1175,9 +1169,9 @@
}
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"node_modules/lodash": {
"version": "4.17.21",
@ -1231,11 +1225,6 @@
"safe-buffer": "^5.1.2"
}
},
"node_modules/memoize-one": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
},
"node_modules/miller-rabin": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz",
@ -1751,14 +1740,13 @@
}
},
"node_modules/redoc": {
"version": "2.0.0-rc.56",
"resolved": "https://registry.npmjs.org/redoc/-/redoc-2.0.0-rc.56.tgz",
"integrity": "sha512-ir2TtQ2d/1FqZWIoLmUZ3qvAAnO6jg8dt0SV75TanmfCXpEABcElXWH3mtUf6qKlvgDVt40diDCVuSvyPPxkAw==",
"version": "2.0.0-rc.59",
"resolved": "https://registry.npmjs.org/redoc/-/redoc-2.0.0-rc.59.tgz",
"integrity": "sha512-1Wkj/HSCv5CdtwF7FSZc5L0EeBgI0N7YpAIsatMtfiMHEon0WhuArAkc5rMQ6mQXUPRrqq5Fs6QPc4GpNp6DuA==",
"dependencies": {
"@babel/runtime": "^7.14.0",
"@redocly/openapi-core": "^1.0.0-beta.50",
"@redocly/openapi-core": "^1.0.0-beta.54",
"@redocly/react-dropdown-aria": "^2.0.11",
"@types/node": "^15.6.1",
"classnames": "^2.3.1",
"decko": "^1.2.0",
"dompurify": "^2.2.8",
@ -1767,7 +1755,6 @@
"lunr": "^2.3.9",
"mark.js": "^8.11.1",
"marked": "^0.7.0",
"memoize-one": "^5.2.1",
"mobx-react": "^7.2.0",
"openapi-sampler": "^1.0.1",
"path-browserify": "^1.0.1",
@ -1819,6 +1806,14 @@
"node": ">=0.10.0"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/ripemd160": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
@ -2451,22 +2446,22 @@
"integrity": "sha512-GoXw0U2Qaa33m3eUcxuHnHpNvHjNlLo0gtV091XBpaRINaB4X6FGCG5XKxSFNFiPpugUDqNruHzaqpTdDm4AOg=="
},
"@redocly/ajv": {
"version": "6.12.4",
"resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-6.12.4.tgz",
"integrity": "sha512-RB6vWO78v6c+SW/3bZh+XZMr4nGdJKAiPGsBALuUZnLuCiQ7aXCT1AuFHqnfS2gyXbEUEj+kw8p4ux8KdAfs3A==",
"version": "8.6.2",
"resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.6.2.tgz",
"integrity": "sha512-tU8fQs0D76ZKhJ2cWtnfQthWqiZgGBx0gH0+5D8JvaBEBaqA8foPPBt3Nonwr3ygyv5xrw2IzKWgIY86BlGs+w==",
"requires": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2",
"uri-js": "^4.2.2"
}
},
"@redocly/openapi-core": {
"version": "1.0.0-beta.50",
"resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.0.0-beta.50.tgz",
"integrity": "sha512-GuXn4IETxpbRd8dlAQDQPtvqOpbMvPMeC/e5mv5MOXkLIznNk4vjiQVe6QSCbZbCHzzpb2+89B6S7asebPm4Rg==",
"version": "1.0.0-beta.62",
"resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.0.0-beta.62.tgz",
"integrity": "sha512-g4iPB7qpSNFCOpf/FgKiic+QCaCn4mdNQOWVEPuwpN7l72hlQ7J3YUa9cssJomkJXXxZ1zdP4h208s12LkhwVA==",
"requires": {
"@redocly/ajv": "^6.12.3",
"@redocly/ajv": "^8.6.2",
"@types/node": "^14.11.8",
"colorette": "^1.2.0",
"js-levenshtein": "^1.1.6",
@ -2478,9 +2473,9 @@
},
"dependencies": {
"@types/node": {
"version": "14.17.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.3.tgz",
"integrity": "sha512-e6ZowgGJmTuXa3GyaPbTGxX17tnThl2aSSizrFthQ7m9uLGZBXiGhgE55cjRZTF5kjZvYn9EOPOMljdjwbflxw=="
"version": "14.17.21",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.21.tgz",
"integrity": "sha512-zv8ukKci1mrILYiQOwGSV4FpkZhyxQtuFWGya2GujWg+zVAeRQ4qbaMmWp9vb9889CFA8JECH7lkwCL6Ygg8kA=="
}
}
},
@ -2525,7 +2520,8 @@
"@types/node": {
"version": "15.12.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz",
"integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww=="
"integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==",
"dev": true
},
"ansi-regex": {
"version": "5.0.0",
@ -2841,9 +2837,9 @@
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"colorette": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
"integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w=="
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
"integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="
},
"concat-map": {
"version": "0.0.1",
@ -3064,11 +3060,6 @@
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
},
"fast-safe-stringify": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz",
@ -3273,9 +3264,9 @@
}
},
"json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"lodash": {
"version": "4.17.21",
@ -3320,11 +3311,6 @@
"safe-buffer": "^5.1.2"
}
},
"memoize-one": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
},
"miller-rabin": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz",
@ -3741,14 +3727,13 @@
}
},
"redoc": {
"version": "2.0.0-rc.56",
"resolved": "https://registry.npmjs.org/redoc/-/redoc-2.0.0-rc.56.tgz",
"integrity": "sha512-ir2TtQ2d/1FqZWIoLmUZ3qvAAnO6jg8dt0SV75TanmfCXpEABcElXWH3mtUf6qKlvgDVt40diDCVuSvyPPxkAw==",
"version": "2.0.0-rc.59",
"resolved": "https://registry.npmjs.org/redoc/-/redoc-2.0.0-rc.59.tgz",
"integrity": "sha512-1Wkj/HSCv5CdtwF7FSZc5L0EeBgI0N7YpAIsatMtfiMHEon0WhuArAkc5rMQ6mQXUPRrqq5Fs6QPc4GpNp6DuA==",
"requires": {
"@babel/runtime": "^7.14.0",
"@redocly/openapi-core": "^1.0.0-beta.50",
"@redocly/openapi-core": "^1.0.0-beta.54",
"@redocly/react-dropdown-aria": "^2.0.11",
"@types/node": "^15.6.1",
"classnames": "^2.3.1",
"decko": "^1.2.0",
"dompurify": "^2.2.8",
@ -3757,7 +3742,6 @@
"lunr": "^2.3.9",
"mark.js": "^8.11.1",
"marked": "^0.7.0",
"memoize-one": "^5.2.1",
"mobx-react": "^7.2.0",
"openapi-sampler": "^1.0.1",
"path-browserify": "^1.0.1",
@ -3794,6 +3778,11 @@
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
},
"require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="
},
"ripemd160": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",

View File

@ -1,6 +1,6 @@
{
"name": "redoc-cli",
"version": "0.12.3",
"version": "0.13.2",
"description": "ReDoc's Command Line Interface",
"main": "index.js",
"bin": "index.js",
@ -19,7 +19,7 @@
"node-libs-browser": "^2.2.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"redoc": "2.0.0-rc.56",
"redoc": "2.0.0-rc.59",
"styled-components": "^5.3.0",
"yargs": "^17.0.1"
},

View File

@ -1,26 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>%PAGE_TITLE%</title>
<link rel="icon" href="%PAGE_FAVICON%" />
<style>
body {
margin: 0;
padding: 0;
}
<head>
<meta charset="UTF-8" />
<title>%PAGE_TITLE%</title>
<link rel="icon" href="%PAGE_FAVICON%">
<style>
body {
margin: 0;
padding: 0;
}
redoc {
display: block;
}
</style>
<link
href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700"
rel="stylesheet"
/>
</head>
redoc {
display: block;
}
</style>
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
</head>
<body>
<redoc spec-url="%SPEC_URL%" %REDOC_OPTIONS%></redoc>
<script src="redoc.standalone.js"></script>
</body>
</html>
<body>
<redoc spec-url="%SPEC_URL%" %REDOC_OPTIONS%></redoc>
<script src="redoc.standalone.js"></script>
</body>
</html>

View File

@ -33,14 +33,14 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
},
devServer: {
contentBase: __dirname,
watchContentBase: true,
static: __dirname,
port: 9090,
disableHostCheck: true,
stats: 'minimal',
hot: true,
historyApiFallback: true,
},
stats: {
children: true,
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.json'],
fallback: {
@ -72,7 +72,7 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
{ test: [/\.eot$/, /\.gif$/, /\.woff$/, /\.svg$/, /\.ttf$/], use: 'null-loader' },
{
test: /\.tsx?$/,
use: [getBabelLoader({useBuiltIns: true, hot: true} )],
use: [getBabelLoader({ useBuiltIns: true, hot: true })],
exclude: {
and: [/node_modules/],
not: {

114
docs/deployment/cli.md Normal file
View File

@ -0,0 +1,114 @@
---
title: Using the Redoc CLI
redirectFrom:
- /docs/quickstart/cli/
---
# Using the Redoc CLI
With Redoc's command-line interface you can bundle your OpenAPI definition and API documentation
(made with Redoc) into a zero-dependency HTML file and locally render your
OpenAPI definition with Redoc.
## Step 1 - Install Redoc CLI
You can install the `redoc-cli` package globally using one of the following package managers:
- [npm](https://docs.npmjs.com/about-npm)
- [yarn](https://classic.yarnpkg.com/en/docs/getting-started)
Or you can install `redoc-cli` using [npx](https://www.freecodecamp.org/news/npm-vs-npx-whats-the-difference/).
### Install Redoc CLI with yarn
To install the `redoc-cli` package globally with yarn:
```bash
yarn global add redoc-cli
```
### Install Redoc with npm
To install the `redoc-cli` package globally with npm:
```bash
npm i -g redoc-cli
```
### Install with `npx`
To install the `redoc-cli` package locally with `npx`, navigate to your project
directory in your terminal, then use the following command:
```bash
npx redoc-cli
```
## Step 2 - Use the CLI
### Redoc CLI commands
The CLI includes the following commands:
- **`redoc-cli serve [spec]`:** Starts a local server with Redoc. You must include the required parameter, spec, which is
a reference to an OpenAPI definition. Options include:
- `--ssr`: Implements a server-side rendering model.
- `--watch`: Automatically reloads the server while you edit your OpenAPI definition.
- `--options`: Customizes your output using [Redoc options](https://redoc.ly/docs/api-reference-docs/configuration/).
To add nested options, use dot notation.
- **`redoc-cli bundle [spec]`:** Bundles `spec` and Redoc into a zero-dependency HTML file. Options include:
- `-t` or `--template`: Uses custom [Handlebars](https://handlebarsjs.com/) templates to render your OpenAPI definition.
- `--templateOptions`: Adds template options you want to pass to your
custom Handlebars template. To add options, use dot notation.
- **`--help`:** Prints help text for the Redoc CLI commands and options.
- **`--version`:** Prints the version of the `redoc-cli` package you have installed.
### Redoc CLI examples
#### Bundle
Bundle with the main color changed to `orange`:
```bash
redoc-cli bundle openapi.yaml --options.theme.colors.primary.main=orange
```
Bundle using a custom Handlebars template and add custom `templateOptions`:
```bash
redoc-cli bundle http://petstore.swagger.io/v2/swagger.json -t custom.hbs --templateOptions.metaDescription "Page meta description"
```
Sample Handlebars template:
```handlebars
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8" />
<title>{{title}}</title>
<!-- needed for adaptive design -->
<meta description="{{{templateOptions.metaDescription}}}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
padding: 0;
margin: 0;
}
</style>
{{{redocHead}}}
{{#unless disableGoogleFont}}<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">{{/unless}}
</head>
<body>
{{{redocHTML}}}
</body>
</html>
```
#### Serve
Serve with the `nativeScrollbars` option set to `true`:
```bash
redoc-cli serve openapi/dist.yaml --options.nativeScrollbars
```

41
docs/deployment/docker.md Normal file
View File

@ -0,0 +1,41 @@
---
title: Using the Redoc Docker image
redirectFrom:
- /docs/quickstart/docker/
---
# Using the Redoc Docker image
Redoc is available as a pre-built Docker image in [Docker Hub](https://hub.docker.com/r/redocly/redoc/).
If you have [Docker](https://docs.docker.com/get-docker/) installed, pull the image with the following command:
```docker
docker pull redocly/redoc
```
Then run the image with the following command:
```docker
docker run -p 8080:80 redocly/redoc
```
The preview starts on port 8080, based on the port used in the command,
and can be accessed at `http://localhost:8080`.
To exit the preview, use `control+C`.
By default Redoc starts with a demo Swagger Petstore OpenAPI definition located at
http://petstore.swagger.io/v2/swagger.json. You can update this URL using
the environment variable `SPEC_URL`.
For example:
```bash
docker run -p 8080:80 -e SPEC_URL=https://api.example.com/openapi.json redocly/redoc
```
## Using a Dockerfile
You can also create a Dockerfile with some predefined environment variables. Check out
a sample [Dockerfile](https://github.com/Redocly/redoc/blob/master/config/docker/Dockerfile)
in our code repo.

123
docs/deployment/html.md Normal file
View File

@ -0,0 +1,123 @@
---
title: Using the Redoc HTML element
redirectFrom:
- /docs/quickstart/html/
---
# Using the Redoc HTML element
## Step 1 - Install Redoc
You can install Redoc using one of the following package managers:
- [npm](https://docs.npmjs.com/about-npm)
- [yarn](https://classic.yarnpkg.com/en/docs/getting-started)
:::attention Initialize your package manager
If you do not have a `package.json` file in your project directory,
you need to add one by initializing npm or yarn in your project. Use the command `npm init` for npm,
or `yarn init` for yarn. These initialization commands will lead you through the process
of creating a `package.json` file in your project.
For more information, see
[Creating a package.json file](https://docs.npmjs.com/creating-a-package-json-file)
in the npm documentation or [Yarn init](https://classic.yarnpkg.com/en/docs/cli/init/)
in the yarn documentation.
:::
### Install Redoc with yarn
After navigating to your project directory in your terminal, use the following command:
```bash
yarn add redoc
```
### Install Redoc with npm
After navigating to your project directory in your terminal, use the following command:
```bash
npm i redoc
```
## Step 2 - Reference the Redoc script
You can reference the Redoc script using either a link to the files hosted on a CDN
or the files located in your `node modules` folder.
### CDN link
To reference the Redoc script with a CDN link:
```html
<script src="https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js"> </script>
```
### Node modules link
To reference the Redoc script with a node modules link:
```html
<script src="node_modules/redoc/bundles/redoc.standalone.js"> </script>
```
## Step 3 - Add the <redoc> element
You can add the <redoc> element to your HTML page and reference your OpenAPI
definition using the `spec-url` attribute, or you can initialize Redoc using
a globally exposed Redoc object.
### Using the `spec-url` attribute
To add the <redoc> element with the `spec-url` attribute:
```html
<redoc spec-url="url/to/your/spec"></redoc>
```
#### Examples
```html
<redoc spec-url="http://petstore.swagger.io/v2/swagger.json"></redoc>
```
You can also use a local file (JSON or YAML) in your project, for instance:
```html
<redoc spec-url="dist.json"></redoc>
```
### Using a Redoc object
To add the <redoc> element with a globally exposed Redoc object:
```js
Redoc.init(specOrSpecUrl, options, element, callback)
```
- `specOrSpecUrl`: Either a JSON object with the OpenAPI definition or a URL to the
definition in JSON or YAML format.
- `options`: See [options object](https://redoc.ly/docs/api-reference-docs/configuration/) reference.
- `element`: DOM element Redoc will be inserted into.
- `callback`(optional): Callback to be called after Redoc has been fully rendered.
It is also called on errors with `error` as the first argument.
#### Examples
```html
<script>
Redoc.init('http://petstore.swagger.io/v2/swagger.json', {
scrollYOffset: 50
}, document.getElementById('redoc-container'))
</script>
```
You can also use a local file (JSON or YAML) in your project, for instance:
```html
<script>
Redoc.init('dist.yaml', {
scrollYOffset: 50
}, document.getElementById('redoc-container'))
</script>
```

112
docs/deployment/intro.md Normal file
View File

@ -0,0 +1,112 @@
---
title: Redoc deployment guide
redirectFrom:
- /docs/quickstart/intro/
---
# Redoc deployment guide
Redoc offers multiple options for rendering your OpenAPI definition.
You should select the option that best fits your needs.
The following options are supported:
- **[Live demo](https://redocly.github.io/redoc/):**
The live demo offers a fast way to see how your OpenAPI will render with Redoc.
A version of the Swagger Petstore API is displayed by default. To test it with your own OpenAPI definition, enter the URL for your
definition and select **TRY IT**.
- **[HTML element](./html.md):**
Using the HTML element works well for typical website deployments.
- **[React component](./react.md):**
Using the React component is an option for users with a React-based application.
- **[Docker image](./docker.md):**
Using the Docker image works in a container-based deployment.
- **[CLI](./cli.md):**
Using the CLI is an option for users who prefer to use a command-line interface.
## Before you start
### OpenAPI definition
You will need an OpenAPI definition. For testing purposes, you can use one of the following sample OpenAPI definitions:
- OpenAPI 3.0
- [Rebilly Users OpenAPI Definition](https://raw.githubusercontent.com/Rebilly/api-definitions/main/openapi/users.yaml)
- [Swagger Petstore Sample OpenAPI Definition](https://petstore3.swagger.io/api/v3/openapi.json)
- OpenAPI 2.0
- [Thingful OpenAPI Definition](https://raw.githubusercontent.com/thingful/openapi-spec/master/spec/swagger.yaml)
- [Fitbit Plus OpenAPI Definition](https://raw.githubusercontent.com/TwineHealth/TwineDeveloperDocs/master/spec/swagger.yaml)
:::info OpenAPI specification
For more information on the OpenAPI specification, refer to the [Learning OpenAPI 3](https://redoc.ly/docs/resources/learning-openapi/)
section in the documentation.
:::
### Running Redoc locally
If you want to view your Redoc output locally, you can simulate an HTTP server.
#### Using Redocly OpenAPI CLI
Redocly OpenAPI CLI is an open source command-line tool that includes a command
for simulating an HTTP server to provide a preview of your OpenAPI definition locally.
If you have [OpenAPI CLI](https://redoc.ly/docs/cli/#installation-and-usage) installed, `cd` into your
project directory and run the following command:
```bash
openapi preview-docs openapi.yaml
```
Replace `openapi.yaml` in the example command with the file path to your OpenAPI definition.
By default, without providing a port, the preview starts on port 8080, and can be accessed at `http://localhost:8080`.
To exit the preview, use `control+C`.
You can alter the port if you are using 8080 already, for example:
```bash
openapi preview-docs -p 8888 openapi.yaml
```
Replace `openapi.yaml` in the example command with the file path to your OpenAPI definition.
For more information about the `preview-docs` command, refer to
[OpenAPI CLI commands](https://redoc.ly/docs/cli/commands/preview-docs/#preview-docs) in the OpenAPI CLI documentation.
#### Using Python
If you have [Python 3](https://www.python.org/downloads/) installed, `cd` into your
project directory and run the following command:
```python
python3 -m http.server
```
If you have [Python 2](https://www.python.org/downloads/) installed, `cd` into your
project directory and run the following command:
```python
python -m SimpleHTTPServer 8000
```
The output after entering the command provides the local URL where the preview can be accessed.
To exit the preview, use `control-C`.
#### Using Node.js
If you have [Node.js](https://nodejs.org/en/download/) installed, install `http-server`
using the following npm command:
```bash
npm install -g http-server
```
Then, `cd` into your project directory and run the following command:
```node
http-server
```
The output after entering the command provides the local URL where the preview can be accessed.
To exit the preview, use `control-C`.

80
docs/deployment/react.md Normal file
View File

@ -0,0 +1,80 @@
---
title: Using the Redoc React component
redirectFrom:
- /docs/quickstart/react/
---
# Using the Redoc React component
## Before you start
Install the following dependencies required by Redoc if you do not already have them installed:
- `react`
- `react-dom`
- `mobx`
- `styled-components`
- `core-js`
If you have npm installed, you can install these dependencies using the following command:
```js
npm i react react-dom mobx styled-components core-js
```
## Step 1 - Import the `RedocStandalone` component
```js
import { RedocStandalone } from 'redoc';
```
## Step 2 - Use the component
You can either link to your OpenAPI definition with a URL, using the following format:
```react
<RedocStandalone specUrl="url/to/your/spec"/>
```
Or you can pass your OpenAPI definition as an object, using the following format:
```js
<RedocStandalone spec={/* spec as an object */}/>
```
## Optional - Pass options
Options can be passed into the RedocStandalone component to alter how it renders.
For example:
```js
<RedocStandalone
specUrl="http://petstore.swagger.io/v2/swagger.json"
options={{
nativeScrollbars: true,
theme: { colors: { primary: { main: '#dd5522' } } },
}}
/>
```
For more information on configuration options, refer to the
[Configuration options for Reference docs](https://redoc.ly/docs/api-reference-docs/configuration/)
section of the documentation. Options available for Redoc are noted,
"Supported in Redoc CE".
## Optional - Specify `onLoaded` callback
You can also specify the `onLoaded` callback, which is called each time Redoc
is fully rendered or when an error occurs (with an error as the first argument).
```js
<RedocStandalone
specUrl="http://petstore.swagger.io/v2/swagger.json"
onLoaded={error => {
if (!error) {
console.log('Yay!');
}
}}
/>
```

53
docs/quickstart.md Normal file
View File

@ -0,0 +1,53 @@
---
title: Redoc quickstart guide
---
# Redoc quickstart guide
To render your OpenAPI definition using Redoc, use the following HTML code sample and
replace the `spec-url` attribute with the URL or local file address to your definition.
```html
<!DOCTYPE html>
<html>
<head>
<title>Redoc</title>
<!-- needed for adaptive design -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700"
rel="stylesheet"
/>
<!--
Redoc doesn't change outer page styles
-->
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<!--
Redoc element with link to your OpenAPI definition
-->
<redoc spec-url="http://petstore.swagger.io/v2/swagger.json"></redoc>
<!--
Link to Redoc JavaScript on CDN for rendering standalone element
-->
<script src="https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js"></script>
</body>
</html>
```
:::attention Running Redoc locally requires an HTTP server
Loading local OpenAPI definitions is impossible without running a web server because of issues with
[same-origin policy](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy) and
other security reasons. Refer to [Running Redoc locally](./deployment/intro.md#running-redoc-locally) for
more information.
:::
For a more detailed explanation with step-by-step instructions and additional options for using Redoc, refer to the [Redoc deployment guide](./deployment/intro.md).

View File

@ -1,13 +0,0 @@
redoc:
- group: Quickstart
expanded: false
page: redoc/quickstart/intro.md
pages:
- label: HTML element
page: redoc/quickstart/html.md
- label: React component
page: redoc/quickstart/react.md
- label: Docker image
page: redoc/quickstart/docker.md
- label: Command-line interface
page: redoc/quickstart/cli.md

View File

@ -53,4 +53,10 @@ describe('Search', () => {
getSearchInput().type('int', { force: true });
cy.get('[data-markjs]').should('exist');
});
it('should show proper message when no search results are found', () => {
getSearchResults().should('not.exist');
getSearchInput().type('xzss', { force: true });
getSearchResults().should('exist').should('contain', 'No results found');
});
});

7451
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "redoc",
"version": "2.0.0-rc.56",
"version": "2.0.0-rc.59",
"description": "ReDoc",
"repository": {
"type": "git",
@ -27,6 +27,7 @@
"React.js"
],
"main": "bundles/redoc.lib.js",
"browser": "bundles/redoc.browser.lib.js",
"types": "typings/index.d.ts",
"scripts": {
"start": "webpack serve --mode=development --env playground --hot --config demo/webpack.config.ts",
@ -36,13 +37,14 @@
"unit": "jest --coverage",
"e2e": "cypress run",
"e2e-ci": "cypress run --record",
"bundlesize": "bundlesize",
"bundlesize": "size-limit",
"ts-check": "tsc --noEmit --skipLibCheck",
"cy:open": "cypress open",
"bundle:clean": "rimraf bundles",
"bundle:standalone": "webpack --env production --env standalone --mode=production",
"bundle:lib": "webpack --mode=production && npm run declarations",
"bundle": "npm run bundle:clean && npm run bundle:lib && npm run bundle:standalone",
"bundle:browser": "webpack --env production --env browser --mode=production",
"bundle": "npm run bundle:clean && npm run bundle:lib && npm run bundle:browser && npm run bundle:standalone",
"declarations": "tsc --emitDeclarationOnly -p tsconfig.lib.json && cp -R src/types typings/",
"stats": "webpack --env production --env standalone --json --profile --mode=production > stats.json",
"prettier": "prettier --write \"cli/index.ts\" \"src/**/*.{ts,tsx}\"",
@ -54,7 +56,9 @@
"build:demo": "webpack --mode=production --config demo/webpack.config.ts",
"deploy:demo": "aws s3 sync demo/dist s3://production-redoc-demo --acl=public-read",
"license-check": "license-checker --production --onlyAllow 'MIT;ISC;Apache-2.0;BSD;BSD-2-Clause;BSD-3-Clause' --summary",
"docker:build": "docker build -f config/docker/Dockerfile -t redoc ."
"docker:build": "docker build -f config/docker/Dockerfile -t redoc .",
"prepare": "husky install",
"pre-commit": "pretty-quick --staged && npm run lint"
},
"devDependencies": {
"@babel/core": "^7.14.3",
@ -71,6 +75,7 @@
"@babel/preset-typescript": "^7.13.0",
"@cypress/webpack-preprocessor": "^5.9.0",
"@hot-loader/react-dom": "^17.0.1",
"@size-limit/preset-app": "^7.0.4",
"@types/chai": "^4.2.18",
"@types/dompurify": "^2.2.2",
"@types/enzyme": "^3.10.5",
@ -79,9 +84,9 @@
"@types/json-pointer": "^1.0.30",
"@types/lodash": "^4.14.170",
"@types/lunr": "^2.3.3",
"@types/node": "^15.6.1",
"@types/mark.js": "^8.11.5",
"@types/marked": "^1.1.0",
"@types/marked": "^3.0.1",
"@types/node": "^15.6.1",
"@types/prismjs": "^1.16.5",
"@types/prop-types": "^15.7.3",
"@types/react": "^17.0.8",
@ -98,7 +103,6 @@
"babel-loader": "^8.2.2",
"babel-plugin-styled-components": "^1.12.0",
"beautify-benchmark": "^0.2.4",
"bundlesize": "^0.18.1",
"conventional-changelog-cli": "^2.0.34",
"copy-webpack-plugin": "^9.0.0",
"core-js": "^3.13.1",
@ -112,18 +116,21 @@
"eslint-plugin-react": "^7.24.0",
"fork-ts-checker-webpack-plugin": "^6.2.10",
"html-webpack-plugin": "^5.3.1",
"husky": "^7.0.0",
"jest": "^27.0.3",
"js-yaml": "^4.1.0",
"license-checker": "^25.0.1",
"lodash": "^4.17.21",
"mobx": "^6.3.2",
"prettier": "^2.3.0",
"prettier": "^2.3.2",
"pretty-quick": "^3.0.0",
"raf": "^3.4.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-hot-loader": "^4.13.0",
"rimraf": "^3.0.2",
"shelljs": "^0.8.4",
"size-limit": "^7.0.4",
"style-loader": "^2.0.0",
"styled-components": "^5.3.0",
"ts-jest": "^27.0.2",
@ -134,7 +141,7 @@
"url-polyfill": "^1.1.12",
"webpack": "^5.38.1",
"webpack-cli": "^4.7.2",
"webpack-dev-server": "^3.11.2",
"webpack-dev-server": "^4.6.0",
"webpack-node-externals": "^3.0.0",
"workerize-loader": "github:redocly/workerize-loader#webpack-5-dist"
},
@ -156,8 +163,7 @@
"json-pointer": "^0.6.1",
"lunr": "^2.3.9",
"mark.js": "^8.11.1",
"marked": "^0.7.0",
"memoize-one": "^5.2.1",
"marked": "^3.0.4",
"mobx-react": "^7.2.0",
"openapi-sampler": "^1.0.1",
"path-browserify": "^1.0.1",
@ -171,10 +177,10 @@
"swagger2openapi": "^7.0.6",
"url-template": "^2.0.8"
},
"bundlesize": [
"size-limit": [
{
"path": "./bundles/redoc.standalone.js",
"maxSize": "350 kB"
"limit": "350 kB"
}
],
"jest": {
@ -207,6 +213,6 @@
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"parser": "typescript"
"arrowParens": "avoid"
}
}

View File

@ -28,16 +28,16 @@ export const StyledDropdown = styled(Dropdown)`
width: auto;
background: white;
color: #263238;
font-family: ${(props) => props.theme.typography.headings.fontFamily};
font-family: ${props => props.theme.typography.headings.fontFamily};
font-size: 0.929em;
line-height: 1.5em;
cursor: pointer;
transition: border 0.25s ease, color 0.25s ease, box-shadow 0.25s ease;
&:hover,
&:focus-within {
border: 1px solid ${(props) => props.theme.colors.primary.main};
color: ${(props) => props.theme.colors.primary.main};
box-shadow: 0px 0px 0px 1px ${(props) => props.theme.colors.primary.main};
border: 1px solid ${props => props.theme.colors.primary.main};
color: ${props => props.theme.colors.primary.main};
box-shadow: 0px 0px 0px 1px ${props => props.theme.colors.primary.main};
}
.dropdown-selector {
display: inline-flex;
@ -48,7 +48,7 @@ export const StyledDropdown = styled(Dropdown)`
margin-bottom: 5px;
}
.dropdown-selector-value {
font-family: ${(props) => props.theme.typography.headings.fontFamily};
font-family: ${props => props.theme.typography.headings.fontFamily};
position: relative;
font-size: 0.929em;
width: 100%;
@ -63,7 +63,7 @@ export const StyledDropdown = styled(Dropdown)`
right: 3px;
top: 50%;
transform: translateY(-50%);
border-color: ${(props) => props.theme.colors.primary.main} transparent transparent;
border-color: ${props => props.theme.colors.primary.main} transparent transparent;
border-style: solid;
border-width: 0.35em 0.35em 0;
width: 0;
@ -128,8 +128,8 @@ export const SimpleDropdown = styled(StyledDropdown)`
border: none;
box-shadow: none;
.dropdown-selector-value {
color: ${(props) => props.theme.colors.primary.main};
text-shadow: 0px 0px 0px ${(props) => props.theme.colors.primary.main};
color: ${props => props.theme.colors.primary.main};
text-shadow: 0px 0px 0px ${props => props.theme.colors.primary.main};
}
}
}

View File

@ -1,5 +1,3 @@
// import { transparentize } from 'polished';
import styled, { extensionsHook, media } from '../styled-components';
import { deprecatedCss } from './mixins';
@ -68,7 +66,7 @@ export const PropertyNameCell = styled(PropertyCell)`
line-height: 20px;
white-space: nowrap;
font-size: 13px;
font-family: ${(props) => props.theme.typography.code.fontFamily};
font-family: ${props => props.theme.typography.code.fontFamily};
&.deprecated {
${deprecatedCss};
@ -82,7 +80,7 @@ export const PropertyNameCell = styled(PropertyCell)`
export const PropertyDetailsCell = styled.td`
border-bottom: 1px solid #9fb4be;
padding: 10px 0;
width: ${(props) => props.theme.schema.defaultDetailsWidth};
width: ${props => props.theme.schema.defaultDetailsWidth};
box-sizing: border-box;
tr.expanded & {
@ -92,7 +90,7 @@ export const PropertyDetailsCell = styled.td`
${media.lessThan('small')`
padding: 0 20px;
border-bottom: none;
border-left: 1px solid ${(props) => props.theme.schema.linesColor};
border-left: 1px solid ${props => props.theme.schema.linesColor};
tr.last > & {
border-left: none;

View File

@ -6,7 +6,7 @@ import styled, { css } from '../styled-components';
import { HistoryService } from '../services';
// tslint:disable-next-line
export const linkifyMixin = (className) => css`
export const linkifyMixin = className => css`
${className} {
cursor: pointer;
margin-left: -20px;
@ -33,13 +33,13 @@ export const linkifyMixin = (className) => css`
}
`;
const isModifiedEvent = (event) =>
const isModifiedEvent = event =>
!!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
export function Link(props: { to: string; className?: string; children?: any }) {
const store = React.useContext(StoreContext);
const clickHandler = React.useCallback(
(event: React.MouseEvent<HTMLAnchorElement>) => {
(event: React.MouseEvent<HTMLAnchorElement>) => {
if (!store) return;
navigate(store.menu.history, event, props.to);
},

View File

@ -12,7 +12,7 @@ import styled, { createGlobalStyle } from '../styled-components';
* That's why the following ugly fix is required
*/
const PerfectScrollbarConstructor =
PerfectScrollbarNamespace.default || ((PerfectScrollbarNamespace as any) as PerfectScrollbarType);
PerfectScrollbarNamespace.default || (PerfectScrollbarNamespace as any as PerfectScrollbarType);
const PSStyling = createGlobalStyle`${psStyles && psStyles.toString()}`;

View File

@ -39,7 +39,12 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
const license =
(info.license && (
<InfoSpan>
License: {info.license.identifier ? info.license.identifier : (<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>
)) ||
null;
@ -101,8 +106,8 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
)) ||
null}
</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.summary} data-role="redoc-summary" />
<Markdown source={store.spec.info.description} data-role="redoc-description" />
{externalDocs && <ExternalDocumentation externalDocs={externalDocs} />}
</MiddlePanel>
</Row>

View File

@ -48,10 +48,10 @@ const CallbackTitleWrapper = styled.button`
`;
const CallbackName = styled.span<{ deprecated?: boolean }>`
text-decoration: ${(props) => (props.deprecated ? 'line-through' : 'none')};
text-decoration: ${props => (props.deprecated ? 'line-through' : 'none')};
margin-right: 8px;
`;
const OperationBadgeStyled = styled(OperationBadge)`
margin: 0px 5px 0px 0px;
margin: 0 5px 0 0;
`;

View File

@ -35,7 +35,7 @@ export const EndpointInfo = styled.button<{ expanded?: boolean; inverted?: boole
(props.expanded && !props.inverted && `border-color: ${props.theme.colors.border.dark};`) || ''}
.${ServerRelativeURL} {
color: ${props => (props.inverted ? props.theme.colors.text.primary : '#ffffff')}
color: ${props => (props.inverted ? props.theme.colors.text.primary : '#ffffff')};
}
&:focus {
box-shadow: inset 0 2px 2px rgba(0, 0, 0, 0.45), 0 2px 0 rgba(128, 128, 128, 0.25);

View File

@ -59,7 +59,7 @@ export class EnumValues extends React.PureComponent<EnumValuesProps, EnumValuesS
{values.length === 1 ? l('enumSingleValue') : l('enum')}:
</FieldLabel>{' '}
{displayedItems.map((value, idx) => {
const exampleValue = enumSkipQuotes ? value : JSON.stringify(value);
const exampleValue = enumSkipQuotes ? String(value) : JSON.stringify(value);
return (
<React.Fragment key={idx}>
<ExampleValue>{exampleValue}</ExampleValue>{' '}

View File

@ -13,7 +13,7 @@ export class FieldDetail extends React.PureComponent<FieldDetailProps> {
return null;
}
const value = this.props.raw ? this.props.value : JSON.stringify(this.props.value);
const value = this.props.raw ? String(this.props.value) : JSON.stringify(this.props.value);
return (
<div>

View File

@ -45,7 +45,9 @@ export class FieldDetails extends React.PureComponent<FieldProps, { patternShown
} else {
const label = l('example') + ':';
const raw = !!field.in;
renderedExamples = <FieldDetail label={label} value={getSerializedValue(field, field.example)} raw={raw} />;
renderedExamples = (
<FieldDetail label={label} value={getSerializedValue(field, field.example)} raw={raw} />
);
}
}
@ -64,21 +66,23 @@ export class FieldDetails extends React.PureComponent<FieldProps, { patternShown
)}
{schema.contentEncoding && (
<TypeFormat>
{' '}&lt;
{' '}
&lt;
{schema.contentEncoding}
&gt;{' '}
</TypeFormat>
)}
{schema.contentMediaType && (
<TypeFormat>
{' '}&lt;
{' '}
&lt;
{schema.contentMediaType}
&gt;{' '}
</TypeFormat>
)}
{schema.title && !hideSchemaTitles && <TypeTitle> ({schema.title}) </TypeTitle>}
<ConstraintsView constraints={schema.constraints} />
<Pattern schema={schema}/>
<Pattern schema={schema} />
{schema.isCircular && <RecursiveLabel> {l('recursive')} </RecursiveLabel>}
</div>
{deprecated && (
@ -87,7 +91,9 @@ export class FieldDetails extends React.PureComponent<FieldProps, { patternShown
</div>
)}
<FieldDetail raw={rawDefault} label={l('default') + ':'} value={schema.default} />
{!renderDiscriminatorSwitch && <EnumValues isArrayType={isArrayType} values={schema.enum} />}{' '}
{!renderDiscriminatorSwitch && (
<EnumValues isArrayType={isArrayType} values={schema.enum} />
)}{' '}
{renderedExamples}
{<Extensions extensions={{ ...field.extensions, ...schema.extensions }} />}
<div>
@ -97,8 +103,8 @@ export class FieldDetails extends React.PureComponent<FieldProps, { patternShown
<ExternalDocumentation externalDocs={schema.externalDocs} compact={true} />
)}
{(renderDiscriminatorSwitch && renderDiscriminatorSwitch(this.props)) || null}
{field.const && (<FieldDetail label={l('const') + ':'} value={field.const} />) || null}
{isArrayType && schema.items && <ArrayItemDetails schema={schema.items}/>}
{(field.const && <FieldDetail label={l('const') + ':'} value={field.const} />) || null}
{isArrayType && schema.items && <ArrayItemDetails schema={schema.items} />}
</div>
);
}

View File

@ -34,11 +34,11 @@ class Json extends React.PureComponent<JsonProps> {
<button onClick={this.collapseAll}> Collapse all </button>
</SampleControls>
<OptionsContext.Consumer>
{(options) => (
{options => (
<PrismDiv
className={this.props.className}
// tslint:disable-next-line
ref={(node) => (this.node = node!)}
ref={node => (this.node = node!)}
dangerouslySetInnerHTML={{
__html: jsonToHTML(this.props.data, options.jsonSampleExpandLevel),
}}

View File

@ -6,8 +6,8 @@ export const jsonStyles = css`
pointer-events: none;
}
font-family: ${(props) => props.theme.typography.code.fontFamily};
font-size: ${(props) => props.theme.typography.code.fontSize};
font-family: ${props => props.theme.typography.code.fontFamily};
font-size: ${props => props.theme.typography.code.fontSize};
white-space: ${({ theme }) => (theme.typography.code.wrap ? 'pre-wrap' : 'pre')};
contain: content;
@ -51,8 +51,8 @@ export const jsonStyles = css`
background-color: transparent;
border: 0;
color: #fff;
font-family: ${(props) => props.theme.typography.code.fontFamily};
font-size: ${(props) => props.theme.typography.code.fontSize};
font-family: ${props => props.theme.typography.code.fontFamily};
font-size: ${props => props.theme.typography.code.fontSize};
padding-right: 6px;
padding-left: 6px;
padding-top: 0;

View File

@ -26,7 +26,6 @@ export const StyledMarkdownBlock = styled(
{ compact?: boolean; inline?: boolean }
>,
)`
font-family: ${props => props.theme.typography.fontFamily};
font-weight: ${props => props.theme.typography.fontWeightRegular};
line-height: ${props => props.theme.typography.lineHeight};
@ -81,7 +80,7 @@ export const StyledMarkdownBlock = styled(
pre {
font-family: ${props => props.theme.typography.code.fontFamily};
white-space:${({ theme }) => (theme.typography.code.wrap ? 'pre-wrap' : 'pre')};
white-space: ${({ theme }) => (theme.typography.code.wrap ? 'pre-wrap' : 'pre')};
background-color: ${({ theme }) => theme.codeBlock.backgroundColor};
color: white;
padding: ${props => props.theme.spacing.unit * 4}px;
@ -121,7 +120,8 @@ export const StyledMarkdownBlock = styled(
margin: 0;
margin-bottom: 1em;
ul, ol {
ul,
ol {
margin-bottom: 0;
margin-top: 0;
}

View File

@ -18,12 +18,6 @@ import { ResponsesList } from '../Responses/ResponsesList';
import { ResponseSamples } from '../ResponseSamples/ResponseSamples';
import { SecurityRequirements } from '../SecurityRequirement/SecurityRequirement';
const OperationRow = styled(Row)`
backface-visibility: hidden;
contain: content;
overflow: hidden;
`;
const Description = styled.div`
margin-bottom: ${({ theme }) => theme.spacing.unit * 6}px;
`;
@ -42,8 +36,8 @@ export class Operation extends React.Component<OperationProps> {
return (
<OptionsContext.Consumer>
{(options) => (
<OperationRow>
{options => (
<Row>
<MiddlePanel>
<H2>
<ShareLink to={operation.id} />
@ -71,7 +65,7 @@ export class Operation extends React.Component<OperationProps> {
<ResponseSamples operation={operation} />
<CallbackSamples callbacks={operation.callbacks} />
</DarkRightPanel>
</OperationRow>
</Row>
)}
</OptionsContext.Consumer>
);

View File

@ -67,7 +67,10 @@ function DropdownWithinHeader(props) {
);
}
export function BodyContent(props: { content: MediaContentModel; description?: string }): JSX.Element {
export function BodyContent(props: {
content: MediaContentModel;
description?: string;
}): JSX.Element {
const { content, description } = props;
const { isRequestType } = content;
return (

View File

@ -1,6 +1,10 @@
import * as React from 'react';
import { argValueToBoolean, RedocNormalizedOptions, RedocRawOptions } from '../services/RedocNormalizedOptions';
import {
argValueToBoolean,
RedocNormalizedOptions,
RedocRawOptions,
} from '../services/RedocNormalizedOptions';
import { ErrorBoundary } from './ErrorBoundary';
import { Loading } from './Loading/Loading';
import { Redoc } from './Redoc/Redoc';
@ -32,4 +36,4 @@ export const RedocStandalone = function (props: RedocStandaloneProps) {
</StoreBuilder>
</ErrorBoundary>
);
}
};

View File

@ -1,39 +1,47 @@
import { observer } from 'mobx-react';
import * as React from 'react';
import { observer } from 'mobx-react';
import { ResponseModel } from '../../services/models';
import type { ResponseModel, MediaTypeModel } from '../../services/models';
import { ResponseDetails } from './ResponseDetails';
import { ResponseDetailsWrap, StyledResponseTitle } from './styled.elements';
@observer
export class ResponseView extends React.Component<{ response: ResponseModel }> {
toggle = () => {
this.props.response.toggle();
};
render() {
const { headers, type, summary, description, code, expanded, content } = this.props.response;
const mimes =
content === undefined ? [] : content.mediaTypes.filter(mime => mime.schema !== undefined);
const empty = headers.length === 0 && mimes.length === 0 && !description;
return (
<div>
<StyledResponseTitle
onClick={this.toggle}
type={type}
empty={empty}
title={summary || ''}
code={code}
opened={expanded}
/>
{expanded && !empty && (
<ResponseDetailsWrap>
<ResponseDetails response={this.props.response} />
</ResponseDetailsWrap>
)}
</div>
);
}
export interface ResponseViewProps {
response: ResponseModel;
}
export const ResponseView = observer(({ response }: ResponseViewProps): React.ReactElement => {
const { extensions, headers, type, summary, description, code, expanded, content } = response;
const mimes = React.useMemo<MediaTypeModel[]>(
() =>
content === undefined ? [] : content.mediaTypes.filter(mime => mime.schema !== undefined),
[content],
);
const empty = React.useMemo<boolean>(
() =>
(!extensions || Object.keys(extensions).length === 0) &&
headers.length === 0 &&
mimes.length === 0 &&
!description,
[extensions, headers, mimes, description],
);
return (
<div>
<StyledResponseTitle
onClick={response.toggle}
type={type}
empty={empty}
title={summary || ''}
code={code}
opened={expanded}
/>
{expanded && !empty && (
<ResponseDetailsWrap>
<ResponseDetails response={response} />
</ResponseDetailsWrap>
)}
</div>
);
});

View File

@ -7,15 +7,17 @@ import { DropdownOrLabel } from '../DropdownOrLabel/DropdownOrLabel';
import { MediaTypesSwitch } from '../MediaTypeSwitch/MediaTypesSwitch';
import { Schema } from '../Schema';
import { Extensions } from '../Fields/Extensions';
import { Markdown } from '../Markdown/Markdown';
import { ResponseHeaders } from './ResponseHeaders';
export class ResponseDetails extends React.PureComponent<{ response: ResponseModel }> {
render() {
const { description, headers, content } = this.props.response;
const { description, extensions, headers, content } = this.props.response;
return (
<>
{description && <Markdown source={description} />}
<Extensions extensions={extensions} />
<ResponseHeaders headers={headers} />
<MediaTypesSwitch content={content} renderDropdown={this.renderDropdown}>
{({ schema }) => {

View File

@ -14,27 +14,34 @@ export interface ResponseTitleProps {
onClick?: () => void;
}
export class ResponseTitle extends React.PureComponent<ResponseTitleProps> {
render() {
const { title, type, empty, code, opened, className, onClick } = this.props;
return (
<button
className={className}
onClick={(!empty && onClick) || undefined}
aria-expanded={opened}
disabled={empty}
>
{!empty && (
<ShelfIcon
size={'1.5em'}
color={type}
direction={opened ? 'down' : 'right'}
float={'left'}
/>
)}
<Code>{code} </Code>
<Markdown compact={true} inline={true} source={title} />
</button>
);
}
function ResponseTitleComponent({
title,
type,
empty,
code,
opened,
className,
onClick,
}: ResponseTitleProps): React.ReactElement {
return (
<button
className={className}
onClick={(!empty && onClick) || undefined}
aria-expanded={opened}
disabled={empty}
>
{!empty && (
<ShelfIcon
size={'1.5em'}
color={type}
direction={opened ? 'down' : 'right'}
float={'left'}
/>
)}
<Code>{code} </Code>
<Markdown compact={true} inline={true} source={title} />
</button>
);
}
export const ResponseTitle = React.memo<ResponseTitleProps>(ResponseTitleComponent);

View File

@ -1,5 +1,3 @@
// import { transparentize } from 'polished';
import { UnderlinedHeader } from '../../common-elements';
import styled from '../../styled-components';
import { ResponseTitle } from './ResponseTitle';
@ -13,16 +11,14 @@ export const StyledResponseTitle = styled(ResponseTitle)`
border-radius: 2px;
margin-bottom: 4px;
line-height: 1.5em;
background-color: #f2f2f2;
cursor: pointer;
color: ${(props) => props.theme.colors.responses[props.type].color};
background-color: ${(props) => props.theme.colors.responses[props.type].backgroundColor};
color: ${props => props.theme.colors.responses[props.type].color};
background-color: ${props => props.theme.colors.responses[props.type].backgroundColor};
&:focus {
outline: auto;
outline-color: ${(props) => props.theme.colors.responses[props.type].color};
outline: auto ${props => props.theme.colors.responses[props.type].color};
}
${(props) =>
${props =>
(props.empty &&
`
cursor: default;

View File

@ -16,14 +16,17 @@ export class ArraySchema extends React.PureComponent<SchemaProps> {
const schema = this.props.schema;
const itemsSchema = schema.items;
const minMaxItems = schema.minItems === undefined && schema.maxItems === undefined ?
'' :
`(${humanizeConstraints(schema)})`;
const minMaxItems =
schema.minItems === undefined && schema.maxItems === undefined
? ''
: `(${humanizeConstraints(schema)})`;
if (schema.displayType && !itemsSchema && !minMaxItems.length) {
return (<div>
<TypeName>{schema.displayType}</TypeName>
</div>);
return (
<div>
<TypeName>{schema.displayType}</TypeName>
</div>
);
}
return (

View File

@ -18,37 +18,35 @@ export interface ObjectSchemaProps extends SchemaProps {
};
}
@observer
export class ObjectSchema extends React.Component<ObjectSchemaProps> {
static contextType = OptionsContext;
export const ObjectSchema = observer(
({
schema: { fields = [], title },
showTitle,
discriminator,
skipReadOnly,
skipWriteOnly,
}: ObjectSchemaProps) => {
const { expandSingleSchemaField, showObjectSchemaExamples } = React.useContext(OptionsContext);
get parentSchema() {
return this.props.discriminator!.parentSchema;
}
const filteredFields = React.useMemo(
() =>
skipReadOnly || skipWriteOnly
? fields.filter(
item =>
!(
(skipReadOnly && item.schema.readOnly) ||
(skipWriteOnly && item.schema.writeOnly)
),
)
: fields,
[skipReadOnly, skipWriteOnly, fields],
);
render() {
const {
schema: { fields = [] },
showTitle,
discriminator,
} = this.props;
const needFilter = this.props.skipReadOnly || this.props.skipWriteOnly;
const filteredFields = needFilter
? fields.filter(item => {
return !(
(this.props.skipReadOnly && item.schema.readOnly) ||
(this.props.skipWriteOnly && item.schema.writeOnly)
);
})
: fields;
const expandByDefault = this.context.expandSingleSchemaField && filteredFields.length === 1;
const expandByDefault = expandSingleSchemaField && filteredFields.length === 1;
return (
<PropertiesTable>
{showTitle && <PropertiesTableCaption>{this.props.schema.title}</PropertiesTableCaption>}
{showTitle && <PropertiesTableCaption>{title}</PropertiesTableCaption>}
<tbody>
{mapWithLast(filteredFields, (field, isLast) => {
return (
@ -58,26 +56,25 @@ export class ObjectSchema extends React.Component<ObjectSchemaProps> {
field={field}
expandByDefault={expandByDefault}
renderDiscriminatorSwitch={
(discriminator &&
discriminator.fieldName === field.name &&
(() => (
<DiscriminatorDropdown
parent={this.parentSchema}
enumValues={field.schema.enum}
/>
))) ||
undefined
discriminator?.fieldName === field.name
? () => (
<DiscriminatorDropdown
parent={discriminator!.parentSchema}
enumValues={field.schema.enum}
/>
)
: undefined
}
className={field.expanded ? 'expanded' : undefined}
showExamples={false}
skipReadOnly={this.props.skipReadOnly}
skipWriteOnly={this.props.skipWriteOnly}
showTitle={this.props.showTitle}
showExamples={showObjectSchemaExamples}
skipReadOnly={skipReadOnly}
skipWriteOnly={skipWriteOnly}
showTitle={showTitle}
/>
);
})}
</tbody>
</PropertiesTable>
);
}
}
},
);

View File

@ -73,7 +73,7 @@ export class Schema extends React.Component<Partial<SchemaProps>> {
}
// TODO: maybe adjust FieldDetails to accept schema
const field = ({
const field = {
schema,
name: '',
required: false,
@ -82,7 +82,7 @@ export class Schema extends React.Component<Partial<SchemaProps>> {
deprecated: false,
toggle: () => null,
expanded: false,
} as any) as FieldModel; // cast needed for hot-loader to not fail
} as any as FieldModel; // cast needed for hot-loader to not fail
return (
<div>

View File

@ -16,6 +16,7 @@ import {
SearchResultsBox,
SearchWrap,
} from './styled.elements';
import { l } from '../../services/Labels';
export interface SearchBoxProps {
search: SearchStore<string>;
@ -28,6 +29,7 @@ export interface SearchBoxProps {
export interface SearchBoxState {
results: SearchResult[];
noResults: boolean;
term: string;
activeItemIdx: number;
}
@ -39,6 +41,7 @@ export class SearchBox extends React.PureComponent<SearchBoxProps, SearchBoxStat
super(props);
this.state = {
results: [],
noResults: false,
term: '',
activeItemIdx: -1,
};
@ -47,6 +50,7 @@ export class SearchBox extends React.PureComponent<SearchBoxProps, SearchBoxStat
clearResults(term: string) {
this.setState({
results: [],
noResults: false,
term,
});
this.props.marker.unmark();
@ -55,6 +59,7 @@ export class SearchBox extends React.PureComponent<SearchBoxProps, SearchBoxStat
clear = () => {
this.setState({
results: [],
noResults: false,
term: '',
activeItemIdx: -1,
});
@ -95,6 +100,7 @@ export class SearchBox extends React.PureComponent<SearchBoxProps, SearchBoxStat
setResults(results: SearchResult[], term: string) {
this.setState({
results,
noResults: results.length === 0,
});
this.props.marker.mark(term);
}
@ -166,6 +172,9 @@ export class SearchBox extends React.PureComponent<SearchBoxProps, SearchBoxStat
</SearchResultsBox>
</PerfectScrollbarWrap>
)}
{this.state.term && this.state.noResults ? (
<SearchResultsBox data-role="search:results">{l('noResultsFound')}</SearchResultsBox>
) : null}
</SearchWrap>
);
}

View File

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

View File

@ -1,4 +1,3 @@
// import { observe } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
@ -8,6 +7,7 @@ import { shortenHTTPVerb } from '../../utils/openapi';
import { MenuItems } from './MenuItems';
import { MenuItemLabel, MenuItemLi, MenuItemTitle, OperationBadge } from './styled.elements';
import { l } from '../../services/Labels';
import { scrollIntoViewIfNeeded } from '../../utils';
export interface MenuItemProps {
item: IMenuItem;
@ -34,7 +34,7 @@ export class MenuItem extends React.Component<MenuItemProps> {
scrollIntoViewIfActive() {
if (this.props.item.active && this.ref.current) {
this.ref.current.scrollIntoViewIfNeeded();
scrollIntoViewIfNeeded(this.ref.current);
}
}
@ -46,8 +46,8 @@ export class MenuItem extends React.Component<MenuItemProps> {
<OperationMenuItemContent {...this.props} item={item as OperationModel} />
) : (
<MenuItemLabel depth={item.depth} active={item.active} type={item.type} ref={this.ref}>
<MenuItemTitle title={item.name}>
{item.name}
<MenuItemTitle title={item.sidebarLabel}>
{item.sidebarLabel}
{this.props.children}
</MenuItemTitle>
{(item.depth > 0 && item.items.length > 0 && (
@ -78,7 +78,7 @@ export class OperationMenuItemContent extends React.Component<OperationMenuItemC
componentDidUpdate() {
if (this.props.item.active && this.ref.current) {
this.ref.current.scrollIntoViewIfNeeded();
scrollIntoViewIfNeeded(this.ref.current);
}
}
@ -97,7 +97,7 @@ export class OperationMenuItemContent extends React.Component<OperationMenuItemC
<OperationBadge type={item.httpVerb}>{shortenHTTPVerb(item.httpVerb)}</OperationBadge>
)}
<MenuItemTitle width="calc(100% - 38px)">
{item.name}
{item.sidebarLabel}
{this.props.children}
</MenuItemTitle>
</MenuItemLabel>

View File

@ -37,7 +37,6 @@ export class SideMenu extends React.Component<{ menu: MenuStore; className?: str
if (item && item.active && this.context.menuToggle) {
return item.expanded ? item.collapse() : item.expand();
}
this.props.menu.activateAndScroll(item, true);
setTimeout(() => {
if (this._updateScroll) {

View File

@ -30,7 +30,7 @@ const { Provider, Consumer } = StoreContext;
export { Provider as StoreProvider, Consumer as StoreConsumer, StoreContext };
export function StoreBuilder(props: StoreBuilderProps) {
const {spec, specUrl, options, onLoaded, children } = props;
const { spec, specUrl, options, onLoaded, children } = props;
const [resolvedSpec, setResolvedSpec] = React.useState<any>(null);
@ -44,7 +44,7 @@ export function StoreBuilder(props: StoreBuilderProps) {
setResolvedSpec(resolved);
}
load();
}, [spec, specUrl])
}, [spec, specUrl]);
const store = React.useMemo(() => {
if (!resolvedSpec) return null;
@ -62,7 +62,7 @@ export function StoreBuilder(props: StoreBuilderProps) {
if (store && onLoaded) {
onLoaded();
}
}, [store, onLoaded])
}, [store, onLoaded]);
return children({
loading: !store,

View File

@ -26,7 +26,7 @@ describe('Components', () => {
options,
);
const schemaViewElement = shallow(<Schema schema={schema} />).getElement();
expect(schemaViewElement.type).toEqual(ObjectSchema);
expect(schemaViewElement).toMatchSnapshot();
expect(schemaViewElement.props.discriminator).toBeDefined();
expect(schemaViewElement.props.discriminator.parentSchema).toBeDefined();
expect(schemaViewElement.props.discriminator.fieldName).toEqual('type');

View File

@ -10,14 +10,15 @@ 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: {} },
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} />
<SecurityRequirement key={1} security={securityRequirement} />,
).getElement();
expect(securityElement.props.children.type.target).toEqual('span');
expect(securityElement.props.children.props.children).toEqual('None');

View File

@ -10,6 +10,7 @@ export interface LabelsConfig {
arrayOf: string;
webhook: string;
const: string;
noResultsFound: string;
download: string;
downloadSpecification: string;
responses: string;
@ -32,6 +33,7 @@ const labels: LabelsConfig = {
arrayOf: 'Array of ',
webhook: 'Event',
const: 'Value',
noResultsFound: 'No results found',
download: 'Download',
downloadSpecification: 'Download OpenAPI specification',
responses: 'Responses',

View File

@ -1,3 +1,4 @@
import * as React from 'react';
import * as marked from 'marked';
import { highlight, safeSlugify, unescapeHTMLChars } from '../utils';
@ -56,10 +57,12 @@ export class MarkdownRenderer {
headings: MarkdownHeading[] = [];
currentTopHeading: MarkdownHeading;
public parser: marked.Parser; // required initialization, `parser` is used by `marked.Renderer` instance under the hood
private headingEnhanceRenderer: marked.Renderer;
private originalHeadingRule: typeof marked.Renderer.prototype.heading;
constructor(public options?: RedocNormalizedOptions) {
this.parser = new marked.Parser();
this.headingEnhanceRenderer = new marked.Renderer();
this.originalHeadingRule = this.headingEnhanceRenderer.heading.bind(
this.headingEnhanceRenderer,
@ -98,7 +101,7 @@ export class MarkdownRenderer {
attachHeadingsDescriptions(rawText: string) {
const buildRegexp = (heading: MarkdownHeading) => {
return new RegExp(`##?\\s+${heading.name.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}`);
return new RegExp(`##?\\s+${heading.name.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}\s*\n`);
};
const flatHeadings = this.flattenHeadings(this.headings);
@ -121,10 +124,7 @@ export class MarkdownRenderer {
prevRegexp = regexp;
prevPos = currentPos;
}
prevHeading.description = rawText
.substring(prevPos)
.replace(prevRegexp, '')
.trim();
prevHeading.description = rawText.substring(prevPos).replace(prevRegexp, '').trim();
}
headingRule = (
@ -132,7 +132,7 @@ export class MarkdownRenderer {
level: 1 | 2 | 3 | 4 | 5 | 6,
raw: string,
slugger: marked.Slugger,
) => {
): string => {
if (level === 1) {
this.currentTopHeading = this.saveHeading(text, level);
} else if (level === 2) {

View File

@ -12,6 +12,7 @@ import {
SECURITY_DEFINITIONS_COMPONENT_NAME,
setSecuritySchemePrefix,
JsonPointer,
alphabeticallyByProp,
} from '../utils';
import { MarkdownRenderer } from './MarkdownRenderer';
import { GroupModel, OperationModel } from './models';
@ -130,9 +131,11 @@ export class MenuBuilder {
/**
* Returns array of OperationsGroup items for the tags of the group or for all tags
* @param parser
* @param tagsMap tags info returned from `getTagsWithOperations`
* @param parent parent item
* @param group group which this tag belongs to. if not provided gets all tags
* @param options normalized options
*/
static getTagsItems(
parser: OpenAPIParser,
@ -183,14 +186,21 @@ export class MenuBuilder {
res.push(item);
}
if (options.sortTagsAlphabetically) {
res.sort(alphabeticallyByProp<GroupModel | OperationModel>('name'));
}
return res;
}
/**
* Returns array of Operation items for the tag
* @param parser
* @param parent parent OperationsGroup
* @param tag tag info returned from `getTagsWithOperations`
* @param depth items depth
* @param options - normalized options
*/
static getOperationsItems(
parser: OpenAPIParser,
@ -209,6 +219,11 @@ export class MenuBuilder {
operation.depth = depth;
res.push(operation);
}
if (options.sortOperationsAlphabetically) {
res.sort(alphabeticallyByProp<OperationModel>('name'));
}
return res;
}
@ -226,7 +241,7 @@ export class MenuBuilder {
getTags(parser, webhooks, true);
}
if (spec.paths){
if (spec.paths) {
getTags(parser, spec.paths);
}

View File

@ -16,6 +16,7 @@ export interface IMenuItem {
id: string;
absoluteIdx?: number;
name: string;
sidebarLabel: string;
description?: string;
depth: number;
active: boolean;

View File

@ -193,7 +193,10 @@ export class OpenAPIParser {
if (keys.length === 0) {
return resolved;
}
if (mergeAsAllOf && keys.some((k) => k !== 'description' && k !== 'title' && k !== 'externalDocs')) {
if (
mergeAsAllOf &&
keys.some(k => k !== 'description' && k !== 'title' && k !== 'externalDocs')
) {
return {
allOf: [rest, resolved],
};
@ -244,7 +247,7 @@ export class OpenAPIParser {
}
const allOfSchemas = schema.allOf
.map((subSchema) => {
.map(subSchema => {
if (subSchema && subSchema.$ref && used$Refs.has(subSchema.$ref)) {
return undefined;
}
@ -258,7 +261,7 @@ export class OpenAPIParser {
schema: subMerged,
};
})
.filter((child) => child !== undefined) as Array<{
.filter(child => child !== undefined) as Array<{
$ref: string | undefined;
schema: MergedOpenAPISchema;
}>;
@ -337,7 +340,7 @@ export class OpenAPIParser {
const def = this.deref(schemas[defName]);
if (
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];
}
@ -363,7 +366,7 @@ export class OpenAPIParser {
const beforeAllOf = allOf.slice(0, i);
const afterAllOf = allOf.slice(i + 1);
return {
oneOf: sub.oneOf.map((part) => {
oneOf: sub.oneOf.map(part => {
const merged = this.mergeAllOf({
allOf: [...beforeAllOf, part, ...afterAllOf],
});

View File

@ -5,6 +5,11 @@ import { isNumeric, mergeObjects } from '../utils/helpers';
import { LabelsConfigRaw, setRedocLabels } from './Labels';
import { MDXComponentMeta } from './MarkdownRenderer';
export enum SideNavStyleEnum {
SummaryOnly = 'summary-only',
PathOnly = 'path-only',
}
export interface RedocRawOptions {
theme?: ThemeInterface;
scrollYOffset?: number | string | (() => number);
@ -13,6 +18,8 @@ export interface RedocRawOptions {
requiredPropsFirst?: boolean | string;
sortPropsAlphabetically?: boolean | string;
sortEnumValuesAlphabetically?: boolean | string;
sortOperationsAlphabetically?: boolean | string;
sortTagsAlphabetically?: boolean | string;
noAutoAuth?: boolean | string;
nativeScrollbars?: boolean | string;
pathInMiddlePanel?: boolean | string;
@ -22,6 +29,7 @@ export interface RedocRawOptions {
disableSearch?: boolean | string;
onlyRequiredInSamples?: boolean | string;
showExtensions?: boolean | string | string[];
sideNavStyle?: SideNavStyleEnum;
hideSingleRequestSampleTab?: boolean | string;
menuToggle?: boolean | string;
jsonSampleExpandLevel?: number | string | 'all';
@ -29,6 +37,7 @@ export interface RedocRawOptions {
simpleOneOfTypeLabel?: boolean | string;
payloadSampleIdx?: number;
expandSingleSchemaField?: boolean | string;
showObjectSchemaExamples?: boolean | string;
unstable_ignoreMimeParameters?: boolean;
@ -72,7 +81,7 @@ export class RedocNormalizedOptions {
}
if (typeof value === 'string') {
const res = {};
value.split(',').forEach((code) => {
value.split(',').forEach(code => {
res[code.trim()] = true;
});
return res;
@ -138,7 +147,23 @@ export class RedocNormalizedOptions {
case 'false':
return false;
default:
return value.split(',').map((ext) => ext.trim());
return value.split(',').map(ext => ext.trim());
}
}
static normalizeSideNavStyle(value: RedocRawOptions['sideNavStyle']): SideNavStyleEnum {
const defaultValue = SideNavStyleEnum.SummaryOnly;
if (typeof value !== 'string') {
return defaultValue;
}
switch (value) {
case defaultValue:
return value;
case SideNavStyleEnum.PathOnly:
return SideNavStyleEnum.PathOnly;
default:
return defaultValue;
}
}
@ -181,6 +206,8 @@ export class RedocNormalizedOptions {
requiredPropsFirst: boolean;
sortPropsAlphabetically: boolean;
sortEnumValuesAlphabetically: boolean;
sortOperationsAlphabetically: boolean;
sortTagsAlphabetically: boolean;
noAutoAuth: boolean;
nativeScrollbars: boolean;
pathInMiddlePanel: boolean;
@ -189,6 +216,7 @@ export class RedocNormalizedOptions {
disableSearch: boolean;
onlyRequiredInSamples: boolean;
showExtensions: boolean | string[];
sideNavStyle: SideNavStyleEnum;
hideSingleRequestSampleTab: boolean;
menuToggle: boolean;
jsonSampleExpandLevel: number;
@ -197,6 +225,7 @@ export class RedocNormalizedOptions {
simpleOneOfTypeLabel: boolean;
payloadSampleIdx: number;
expandSingleSchemaField: boolean;
showObjectSchemaExamples: boolean;
/* tslint:disable-next-line */
unstable_ignoreMimeParameters: boolean;
@ -239,6 +268,8 @@ export class RedocNormalizedOptions {
this.requiredPropsFirst = argValueToBoolean(raw.requiredPropsFirst);
this.sortPropsAlphabetically = argValueToBoolean(raw.sortPropsAlphabetically);
this.sortEnumValuesAlphabetically = argValueToBoolean(raw.sortEnumValuesAlphabetically);
this.sortOperationsAlphabetically = argValueToBoolean(raw.sortOperationsAlphabetically);
this.sortTagsAlphabetically = argValueToBoolean(raw.sortTagsAlphabetically);
this.noAutoAuth = argValueToBoolean(raw.noAutoAuth);
this.nativeScrollbars = argValueToBoolean(raw.nativeScrollbars);
this.pathInMiddlePanel = argValueToBoolean(raw.pathInMiddlePanel);
@ -247,6 +278,7 @@ export class RedocNormalizedOptions {
this.disableSearch = argValueToBoolean(raw.disableSearch);
this.onlyRequiredInSamples = argValueToBoolean(raw.onlyRequiredInSamples);
this.showExtensions = RedocNormalizedOptions.normalizeShowExtensions(raw.showExtensions);
this.sideNavStyle = RedocNormalizedOptions.normalizeSideNavStyle(raw.sideNavStyle);
this.hideSingleRequestSampleTab = argValueToBoolean(raw.hideSingleRequestSampleTab);
this.menuToggle = argValueToBoolean(raw.menuToggle, true);
this.jsonSampleExpandLevel = RedocNormalizedOptions.normalizeJsonSampleExpandLevel(
@ -257,6 +289,7 @@ export class RedocNormalizedOptions {
this.simpleOneOfTypeLabel = argValueToBoolean(raw.simpleOneOfTypeLabel);
this.payloadSampleIdx = RedocNormalizedOptions.normalizePayloadSampleIdx(raw.payloadSampleIdx);
this.expandSingleSchemaField = argValueToBoolean(raw.expandSingleSchemaField);
this.showObjectSchemaExamples = argValueToBoolean(raw.showObjectSchemaExamples);
this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters);
@ -266,7 +299,7 @@ export class RedocNormalizedOptions {
this.maxDisplayedEnumValues = argValueToNumber(raw.maxDisplayedEnumValues);
const ignoreNamedSchemas = Array.isArray(raw.ignoreNamedSchemas)
? raw.ignoreNamedSchemas
: raw.ignoreNamedSchemas?.split(',').map((s) => s.trim());
: raw.ignoreNamedSchemas?.split(',').map(s => s.trim());
this.ignoreNamedSchemas = new Set(ignoreNamedSchemas);
this.hideSchemaPattern = argValueToBoolean(raw.hideSchemaPattern);
this.generatedPayloadSamplesMaxDepth =

View File

@ -59,7 +59,7 @@ export class SearchStore<T> {
fromExternalJS(path?: string, exportName?: string) {
if (path && exportName) {
this.searchWorker.fromExternalJS(path, exportName)
this.searchWorker.fromExternalJS(path, exportName);
}
}
}

View File

@ -1,12 +1,5 @@
import * as lunr from 'lunr';
try {
// tslint:disable-next-line
require('core-js/es/promise'); // bundle into worker
} catch (_) {
// nope
}
/* just for better typings */
export default class Worker {
add: typeof add = add;

View File

@ -28,7 +28,10 @@ export class SpecStore {
this.externalDocs = this.parser.spec.externalDocs;
this.contentItems = MenuBuilder.buildStructure(this.parser, this.options);
this.securitySchemes = new SecuritySchemesModel(this.parser);
const webhookPath: Referenced<OpenAPIPath> = {...this.parser?.spec?.['x-webhooks'], ...this.parser?.spec.webhooks};
const webhookPath: Referenced<OpenAPIPath> = {
...this.parser?.spec?.['x-webhooks'],
...this.parser?.spec.webhooks,
};
this.webhooks = new WebhookModel(this.parser, options, webhookPath);
}
}

View File

@ -26,6 +26,5 @@ describe('Models', () => {
expect(parser.shallowDeref(schemaOrRef)).toMatchSnapshot();
});
});
});

View File

@ -54,8 +54,8 @@ describe('Models', () => {
license: {
name: 'MIT',
identifier: 'MIT',
url: 'https://opensource.org/licenses/MIT'
}
url: 'https://opensource.org/licenses/MIT',
},
},
} as any;

View File

@ -19,7 +19,6 @@ describe('Models', () => {
expect(contentItems[0].id).toEqual('tag/pet');
expect(contentItems[0].name).toEqual('pet');
expect(contentItems[0].type).toEqual('tag');
});
});
});

View File

@ -20,24 +20,38 @@ describe('Models', () => {
});
test('should calculate response type based on code', () => {
let resp = new ResponseModel({...props, code: '200' });
let resp = new ResponseModel({ ...props, code: '200' });
expect(resp.type).toEqual('success');
resp = new ResponseModel({...props, code: '120' });
resp = new ResponseModel({ ...props, code: '120' });
expect(resp.type).toEqual('info');
resp = new ResponseModel({...props, code: '301' });
resp = new ResponseModel({ ...props, code: '301' });
expect(resp.type).toEqual('redirect');
resp = new ResponseModel({...props, code: '400' });
resp = new ResponseModel({ ...props, code: '400' });
expect(resp.type).toEqual('error');
});
test('default should be successful by default', () => {
const resp = new ResponseModel({...props, code: 'default' });
const resp = new ResponseModel({ ...props, code: 'default' });
expect(resp.type).toEqual('success');
});
test('default should be error if defaultAsError is true', () => {
const resp = new ResponseModel({...props, code: 'default', defaultAsError: true });
const resp = new ResponseModel({ ...props, code: 'default', defaultAsError: true });
expect(resp.type).toEqual('error');
});
test('ensure extensions are shown if showExtensions is true', () => {
const options = new RedocNormalizedOptions({ showExtensions: true });
const resp = new ResponseModel({
parser,
code: 'default',
defaultAsError: true,
infoOrRef: { 'x-example': { a: 1 } },
options,
isEvent: true,
});
expect(Object.keys(resp.extensions).length).toEqual(1);
expect(resp.extensions['x-example']).toEqual({ a: 1 });
});
});
});

View File

@ -14,6 +14,7 @@ export class GroupModel implements IMenuItem {
id: string;
absoluteIdx?: number;
name: string;
sidebarLabel: string;
description?: string;
type: MenuItemGroupType;
@ -43,6 +44,8 @@ export class GroupModel implements IMenuItem {
this.name = tagOrGroup['x-displayName'] || tagOrGroup.name;
this.level = (tagOrGroup as MarkdownHeading).level || 1;
this.sidebarLabel = this.name;
// remove sections from markdown, same as in ApiInfo
this.description = tagOrGroup.description || '';

View File

@ -25,6 +25,7 @@ import { FieldModel } from './Field';
import { MediaContentModel } from './MediaContent';
import { RequestBodyModel } from './RequestBody';
import { ResponseModel } from './Response';
import { SideNavStyleEnum } from '../RedocNormalizedOptions';
export interface XPayloadSample {
lang: 'payload';
@ -49,6 +50,7 @@ export class OperationModel implements IMenuItem {
id: string;
absoluteIdx?: number;
name: string;
sidebarLabel: string;
description?: string;
type = 'operation' as const;
@ -104,11 +106,13 @@ export class OperationModel implements IMenuItem {
this.name = getOperationSummary(operationSpec);
this.sidebarLabel = options.sideNavStyle === SideNavStyleEnum.PathOnly ? this.path : this.name;
if (this.isCallback) {
// NOTE: Callbacks by default should not inherit the specification's global `security` definition.
// Can be defined individually per-callback in the specification. Defaults to none.
this.security = (operationSpec.security || []).map(
(security) => new SecurityRequirementModel(security, parser),
security => new SecurityRequirementModel(security, parser),
);
// TODO: update getting pathInfo for overriding servers on path level
@ -122,7 +126,7 @@ export class OperationModel implements IMenuItem {
: this.pointer;
this.security = (operationSpec.security || parser.spec.security || []).map(
(security) => new SecurityRequirementModel(security, parser),
security => new SecurityRequirementModel(security, parser),
);
this.servers = normalizeServers(
@ -173,7 +177,8 @@ export class OperationModel implements IMenuItem {
@memoize
get requestBody() {
return (
this.operationSpec.requestBody && new RequestBodyModel({
this.operationSpec.requestBody &&
new RequestBodyModel({
parser: this.parser,
infoOrRef: this.operationSpec.requestBody,
options: this.options,
@ -218,7 +223,7 @@ export class OperationModel implements IMenuItem {
this.operationSpec.pathParameters,
this.operationSpec.parameters,
// TODO: fix pointer
).map((paramOrRef) => new FieldModel(this.parser, paramOrRef, this.pointer, this.options));
).map(paramOrRef => new FieldModel(this.parser, paramOrRef, this.pointer, this.options));
if (this.options.sortPropsAlphabetically) {
return sortByField(_parameters, 'name');
@ -234,7 +239,7 @@ export class OperationModel implements IMenuItem {
get responses() {
let hasSuccessResponses = false;
return Object.keys(this.operationSpec.responses || [])
.filter((code) => {
.filter(code => {
if (code === 'default') {
return true;
}
@ -245,7 +250,7 @@ export class OperationModel implements IMenuItem {
return isStatusCode(code);
}) // filter out other props (e.g. x-props)
.map((code) => {
.map(code => {
return new ResponseModel({
parser: this.parser,
code,
@ -259,7 +264,7 @@ export class OperationModel implements IMenuItem {
@memoize
get callbacks() {
return Object.keys(this.operationSpec.callbacks || []).map((callbackEventName) => {
return Object.keys(this.operationSpec.callbacks || []).map(callbackEventName => {
return new CallbackModel(
this.parser,
callbackEventName,

View File

@ -3,28 +3,30 @@ import { OpenAPIRequestBody, Referenced } from '../../types';
import { OpenAPIParser } from '../OpenAPIParser';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
import { MediaContentModel } from './MediaContent';
import { getContentWithLegacyExamples } from '../../utils';
type RequestBodyProps = {
parser: OpenAPIParser;
infoOrRef: Referenced<OpenAPIRequestBody>;
options: RedocNormalizedOptions;
isEvent: boolean;
}
};
export class RequestBodyModel {
description: string;
required: boolean;
content?: MediaContentModel;
constructor(props: RequestBodyProps) {
const { parser, infoOrRef, options, isEvent } = props;
const isRequest = isEvent ? false : true;
constructor({ parser, infoOrRef, options, isEvent }: RequestBodyProps) {
const isRequest = !isEvent;
const info = parser.deref(infoOrRef);
this.description = info.description || '';
this.required = !!info.required;
parser.exitRef(infoOrRef);
if (info.content !== undefined) {
this.content = new MediaContentModel(parser, info.content, isRequest, options);
const mediaContent = getContentWithLegacyExamples(info);
if (mediaContent !== undefined) {
this.content = new MediaContentModel(parser, mediaContent, isRequest, options);
}
}
}

View File

@ -2,20 +2,20 @@ import { action, observable, makeObservable } from 'mobx';
import { OpenAPIResponse, Referenced } from '../../types';
import { getStatusCodeType } from '../../utils';
import { getStatusCodeType, extractExtensions } from '../../utils';
import { OpenAPIParser } from '../OpenAPIParser';
import { RedocNormalizedOptions } from '../RedocNormalizedOptions';
import { FieldModel } from './Field';
import { MediaContentModel } from './MediaContent';
type ResponseProps = {
parser: OpenAPIParser,
code: string,
defaultAsError: boolean,
infoOrRef: Referenced<OpenAPIResponse>,
options: RedocNormalizedOptions,
isEvent: boolean,
}
parser: OpenAPIParser;
code: string;
defaultAsError: boolean;
infoOrRef: Referenced<OpenAPIResponse>;
options: RedocNormalizedOptions;
isEvent: boolean;
};
export class ResponseModel {
@observable
@ -27,10 +27,16 @@ export class ResponseModel {
description: string;
type: string;
headers: FieldModel[] = [];
extensions: Record<string, any>;
constructor(props: ResponseProps) {
const { parser, code, defaultAsError, infoOrRef, options, isEvent } = props;
const isRequest = isEvent ? true : false;
constructor({
parser,
code,
defaultAsError,
infoOrRef,
options,
isEvent: isRequest,
}: ResponseProps) {
makeObservable(this);
this.expanded = options.expandResponses === 'all' || options.expandResponses[code];
@ -59,6 +65,10 @@ export class ResponseModel {
return new FieldModel(parser, { ...header, name }, '', options);
});
}
if (options.showExtensions) {
this.extensions = extractExtensions(info, options.showExtensions);
}
}
@action

View File

@ -134,7 +134,10 @@ export class SchemaModel {
this.maxItems = schema.maxItems;
if (!!schema.nullable || schema['x-nullable']) {
if (Array.isArray(this.type) && !this.type.some((value) => value === null || value === 'null')) {
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'];
@ -142,7 +145,7 @@ export class SchemaModel {
}
this.displayType = Array.isArray(this.type)
? this.type.map(item => item === null ? 'null' : item).join(' or ')
? this.type.map(item => (item === null ? 'null' : item)).join(' or ')
: this.type;
if (this.isCircular) {
@ -155,7 +158,7 @@ export class SchemaModel {
} else if (
isChild &&
Array.isArray(schema.oneOf) &&
schema.oneOf.find((s) => s.$ref === this.pointer)
schema.oneOf.find(s => s.$ref === this.pointer)
) {
// we hit allOf of the schema with the parent discriminator
delete schema.oneOf;
@ -195,8 +198,7 @@ export class SchemaModel {
}
if (Array.isArray(this.type)) {
const filteredType = this.type.filter(item => item !== 'array');
if (filteredType.length)
this.displayType += ` or ${filteredType.join(' or ')}`;
if (filteredType.length) this.displayType += ` or ${filteredType.join(' or ')}`;
}
}
@ -215,7 +217,7 @@ export class SchemaModel {
const title =
isNamedDefinition(variant.$ref) && !merged.title
? JsonPointer.baseName(variant.$ref)
: `${(merged.title || '')}${(merged.const && JSON.stringify(merged.const)) || ''}`;
: `${merged.title || ''}${(merged.const && JSON.stringify(merged.const)) || ''}`;
const schema = new SchemaModel(
parser,
@ -243,7 +245,7 @@ export class SchemaModel {
this.displayType = types.join(' or ');
} else {
this.displayType = this.oneOf
.map((schema) => {
.map(schema => {
let name =
schema.typePrefix +
(schema.title ? `${schema.title} (${schema.displayType})` : schema.displayType);
@ -364,7 +366,7 @@ function buildFields(
const props = schema.properties || {};
const additionalProps = schema.additionalProperties;
const defaults = schema.default;
let fields = Object.keys(props || []).map((fieldName) => {
let fields = Object.keys(props || []).map(fieldName => {
let field = props[fieldName];
if (!field) {

View File

@ -1,6 +1,6 @@
import * as React from 'react';
import { hydrate as hydrateComponent, render } from 'react-dom';
import { configure } from "mobx"
import { configure } from 'mobx';
import { Redoc, RedocStandalone } from './components/';
import { AppStore, StoreState } from './services/AppStore';
@ -8,8 +8,8 @@ import { debugTime, debugTimeEnd } from './utils/debug';
import { querySelector } from './utils/dom';
configure({
useProxies: 'ifavailable'
})
useProxies: 'ifavailable',
});
export { Redoc, AppStore } from '.';

View File

@ -16,7 +16,7 @@ export const media = {
lessThan(breakpoint, print?: boolean, extra?: string) {
return (...args) => css`
@media ${print ? 'print, ' : ''} screen and (max-width: ${props =>
props.theme.breakpoints[breakpoint]})${extra || ''} {
props.theme.breakpoints[breakpoint]}) ${extra || ''} {
${(css as any)(...args)};
}
`;

View File

@ -186,17 +186,20 @@ export interface OpenAPIRequestBody {
description?: string;
required?: boolean;
content: { [mime: string]: OpenAPIMediaType };
'x-examples'?: { [mime: string]: { [name: string]: Referenced<OpenAPIExample> } };
'x-example'?: { [mime: string]: any };
}
export interface OpenAPIResponses {
[code: string]: OpenAPIResponse;
[code: string]: Referenced<OpenAPIResponse>;
}
export interface OpenAPIResponse {
description?: string;
export interface OpenAPIResponse
extends Pick<OpenAPIRequestBody, 'description' | 'x-examples' | 'x-example'> {
headers?: { [name: string]: Referenced<OpenAPIHeader> };
content?: { [mime: string]: OpenAPIMediaType };
links?: { [name: string]: Referenced<OpenAPILink> };
content?: { [mime: string]: OpenAPIMediaType };
}
export interface OpenAPILink {

View File

@ -11,7 +11,9 @@ describe('#loadAndBundleSpec', () => {
});
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 spec = yaml.load(
readFileSync(resolve(__dirname, '../../../demo/openapi-3-1.yaml'), 'utf-8'),
);
const bundledSpec = await loadAndBundleSpec(spec);
expect(bundledSpec).toMatchSnapshot();
});

View File

@ -10,10 +10,17 @@ import {
pluralizeType,
serializeParameterValue,
sortByRequired,
humanizeNumberRange,
getContentWithLegacyExamples,
} from '../';
import { FieldModel, OpenAPIParser, RedocNormalizedOptions } from '../../services';
import { OpenAPIParameter, OpenAPIParameterLocation, OpenAPIParameterStyle } from '../../types';
import {
OpenAPIMediaType,
OpenAPIParameter,
OpenAPIParameterLocation,
OpenAPIParameterStyle,
} from '../../types';
import { expandDefaultServerVariables } from '../openapi';
describe('Utils', () => {
@ -103,7 +110,7 @@ describe('Utils', () => {
it('Should return pathName if no summary, operationId, description', () => {
const operation = {
pathName: '/sandbox/test'
pathName: '/sandbox/test',
};
expect(getOperationSummary(operation as any)).toBe('/sandbox/test');
});
@ -174,7 +181,7 @@ describe('Utils', () => {
expect(isPrimitiveType(schema)).toEqual(false);
});
it('should return true for array contains object and schema hasn\'t properties', () => {
it("should return true for array contains object and schema hasn't properties", () => {
const schema = {
type: ['object', 'string'],
};
@ -231,10 +238,10 @@ describe('Utils', () => {
const schema = {
type: 'array',
items: {
type: 'array',
items: {
type: 'string'
},
type: 'array',
items: {
type: 'string',
},
},
};
expect(isPrimitiveType(schema)).toEqual(true);
@ -410,12 +417,82 @@ describe('Utils', () => {
});
});
describe('openapi humanizeNumberRange', () => {
it('should return `>=` when only minimum value present or exclusiveMinimum = false', () => {
const expected = '>= 0';
expect(humanizeNumberRange({ minimum: 0 })).toEqual(expected);
expect(humanizeNumberRange({ minimum: 0, exclusiveMinimum: false })).toEqual(expected);
});
it('should return `>` when minimum value present and exclusiveMinimum set to true', () => {
expect(humanizeNumberRange({ minimum: 0, exclusiveMinimum: true })).toEqual('> 0');
});
it('should return `<=` when only maximum value present or exclusiveMinimum = false', () => {
const expected = '<= 10';
expect(humanizeNumberRange({ maximum: 10 })).toEqual(expected);
expect(humanizeNumberRange({ maximum: 10, exclusiveMaximum: false })).toEqual(expected);
});
it('should return `<` when maximum value present and exclusiveMaximum set to true', () => {
expect(humanizeNumberRange({ maximum: 10, exclusiveMaximum: true })).toEqual('< 10');
});
it('should return correct range for minimum and maximum values and with different exclusive set', () => {
expect(humanizeNumberRange({ minimum: 0, maximum: 10 })).toEqual('[ 0 .. 10 ]');
expect(
humanizeNumberRange({
minimum: 0,
exclusiveMinimum: true,
maximum: 10,
exclusiveMaximum: true,
}),
).toEqual('( 0 .. 10 )');
expect(
humanizeNumberRange({
minimum: 0,
maximum: 10,
exclusiveMaximum: true,
}),
).toEqual('[ 0 .. 10 )');
expect(
humanizeNumberRange({
minimum: 0,
exclusiveMinimum: true,
maximum: 10,
}),
).toEqual('( 0 .. 10 ]');
});
it('should return correct range exclusive values only', () => {
expect(humanizeNumberRange({ exclusiveMinimum: 0 })).toEqual('> 0');
expect(humanizeNumberRange({ exclusiveMaximum: 10 })).toEqual('< 10');
expect(humanizeNumberRange({ exclusiveMinimum: 0, exclusiveMaximum: 10 })).toEqual(
'( 0 .. 10 )',
);
});
it('should return correct min value', () => {
expect(humanizeNumberRange({ minimum: 5, exclusiveMinimum: 10 })).toEqual('> 5');
expect(humanizeNumberRange({ minimum: -5, exclusiveMinimum: -10 })).toEqual('> -10');
});
it('should return correct max value', () => {
expect(humanizeNumberRange({ maximum: 10, exclusiveMaximum: 15 })).toEqual('< 15');
expect(humanizeNumberRange({ maximum: -10, exclusiveMaximum: -15 })).toEqual('< -10');
});
it('should return undefined', () => {
expect(humanizeNumberRange({})).toEqual(undefined);
});
});
describe('openapi humanizeConstraints', () => {
const itemConstraintSchema = (
min?: number,
max?: number,
multipleOf?: number,
uniqueItems?: boolean
uniqueItems?: boolean,
) => ({ type: 'array', minItems: min, maxItems: max, multipleOf, uniqueItems });
it('should not have a humanized constraint without schema constraints', () => {
@ -455,9 +532,9 @@ describe('Utils', () => {
});
it('should have a humanized constraint when uniqueItems is set', () => {
expect(humanizeConstraints(itemConstraintSchema(undefined, undefined, undefined, true))).toContain(
'unique',
);
expect(
humanizeConstraints(itemConstraintSchema(undefined, undefined, undefined, true)),
).toContain('unique');
});
});
@ -1090,4 +1167,106 @@ describe('Utils', () => {
]);
});
});
describe('OpenAPI getContentWithLegacyExamples', () => {
it('should return undefined if no x-examples/x-example and no content', () => {
expect(getContentWithLegacyExamples({})).toBeUndefined();
});
it('should return unmodified object if no x-examples or x-example', () => {
const info = {
content: {
'application/json': {},
},
};
const content = getContentWithLegacyExamples(info);
expect(content).toStrictEqual(info.content);
});
it('should create a new content object if no content and x-examples', () => {
const info = {
'x-examples': {
'application/json': {
name: {
value: 'test',
},
},
},
};
const content = getContentWithLegacyExamples(info);
expect(content).toEqual({
'application/json': {
examples: {
name: {
value: 'test',
},
},
},
});
});
it('should create a new content object if no content and x-example', () => {
const info = {
'x-example': {
'application/json': 'test',
},
};
const content = getContentWithLegacyExamples(info);
expect(content).toEqual({
'application/json': { example: 'test' },
});
});
it('should return copy of content with injected x-example', () => {
const info = {
'x-example': {
'application/json': 'test',
},
content: {
'application/json': {
schema: { type: 'string' },
},
'text/plain': { schema: { type: 'string' } },
},
};
const content = getContentWithLegacyExamples(info) as { [mime: string]: OpenAPIMediaType };
expect(content).toEqual({
'application/json': { schema: { type: 'string' }, example: 'test' },
'text/plain': { schema: { type: 'string' } },
});
expect(content).not.toStrictEqual(info.content);
expect(content['application/json']).not.toStrictEqual(info.content['application/json']);
expect(content['text/plain']).toStrictEqual(info.content['text/plain']);
});
it('should prefer x-examples over x-example', () => {
const info = {
'x-example': {
'application/json': 'test',
},
'x-examples': {
'application/json': { name: { value: 'test' } },
},
content: {
'application/json': {
schema: { type: 'string' },
},
'text/plain': { schema: { type: 'string' } },
},
};
const content = getContentWithLegacyExamples(info) as { [mime: string]: OpenAPIMediaType };
expect(content).toEqual({
'application/json': { schema: { type: 'string' }, examples: { name: { value: 'test' } } },
'text/plain': { schema: { type: 'string' } },
});
expect(content).not.toStrictEqual(info.content);
expect(content['application/json']).not.toStrictEqual(info.content['application/json']);
expect(content['text/plain']).toStrictEqual(info.content['text/plain']);
});
});
});

View File

@ -12,7 +12,7 @@ function throttle(func, wait) {
context = args = null;
}
};
return function() {
return function () {
const now = new Date().getTime();
const remaining = wait - (now - previous);
context = this;

View File

@ -24,52 +24,54 @@ export function html2Str(html: string): string {
.join(' ');
}
// scrollIntoViewIfNeeded polyfill
// Alternate scrollIntoViewIfNeeded implementation.
// Used in all cases, since it seems Chrome's implementation is buggy
// when "Experimental Web Platform Features" is enabled (at least of version 96).
// See #1714, #1742
if (typeof Element !== 'undefined' && !(Element as any).prototype.scrollIntoViewIfNeeded) {
(Element as any).prototype.scrollIntoViewIfNeeded = function(centerIfNeeded) {
centerIfNeeded = arguments.length === 0 ? true : !!centerIfNeeded;
export function scrollIntoViewIfNeeded(el: HTMLElement, centerIfNeeded = true) {
const parent = el.parentNode as HTMLElement | null;
if (!parent) {
return;
}
const parentComputedStyle = window.getComputedStyle(parent, undefined);
const parentBorderTopWidth = parseInt(
parentComputedStyle.getPropertyValue('border-top-width'),
10,
);
const parentBorderLeftWidth = parseInt(
parentComputedStyle.getPropertyValue('border-left-width'),
10,
);
const overTop = el.offsetTop - parent.offsetTop < parent.scrollTop;
const overBottom =
el.offsetTop - parent.offsetTop + el.clientHeight - parentBorderTopWidth >
parent.scrollTop + parent.clientHeight;
const overLeft = el.offsetLeft - parent.offsetLeft < parent.scrollLeft;
const overRight =
el.offsetLeft - parent.offsetLeft + el.clientWidth - parentBorderLeftWidth >
parent.scrollLeft + parent.clientWidth;
const alignWithTop = overTop && !overBottom;
const parent = this.parentNode;
const parentComputedStyle = window.getComputedStyle(parent, undefined);
const parentBorderTopWidth = parseInt(
parentComputedStyle.getPropertyValue('border-top-width'),
10,
);
const parentBorderLeftWidth = parseInt(
parentComputedStyle.getPropertyValue('border-left-width'),
10,
);
const overTop = this.offsetTop - parent.offsetTop < parent.scrollTop;
const overBottom =
this.offsetTop - parent.offsetTop + this.clientHeight - parentBorderTopWidth >
parent.scrollTop + parent.clientHeight;
const overLeft = this.offsetLeft - parent.offsetLeft < parent.scrollLeft;
const overRight =
this.offsetLeft - parent.offsetLeft + this.clientWidth - parentBorderLeftWidth >
parent.scrollLeft + parent.clientWidth;
const alignWithTop = overTop && !overBottom;
if ((overTop || overBottom) && centerIfNeeded) {
parent.scrollTop =
el.offsetTop -
parent.offsetTop -
parent.clientHeight / 2 -
parentBorderTopWidth +
el.clientHeight / 2;
}
if ((overTop || overBottom) && centerIfNeeded) {
parent.scrollTop =
this.offsetTop -
parent.offsetTop -
parent.clientHeight / 2 -
parentBorderTopWidth +
this.clientHeight / 2;
}
if ((overLeft || overRight) && centerIfNeeded) {
parent.scrollLeft =
el.offsetLeft -
parent.offsetLeft -
parent.clientWidth / 2 -
parentBorderLeftWidth +
el.clientWidth / 2;
}
if ((overLeft || overRight) && centerIfNeeded) {
parent.scrollLeft =
this.offsetLeft -
parent.offsetLeft -
parent.clientWidth / 2 -
parentBorderLeftWidth +
this.clientWidth / 2;
}
if ((overTop || overBottom || overLeft || overRight) && !centerIfNeeded) {
this.scrollIntoView(alignWithTop);
}
};
if ((overTop || overBottom || overLeft || overRight) && !centerIfNeeded) {
el.scrollIntoView(alignWithTop);
}
}

View File

@ -50,7 +50,7 @@ export function flattenByProp<T extends object, P extends keyof T>(
for (const item of items) {
res.push(item);
if (item[prop]) {
iterate((item[prop] as any) as T[]);
iterate(item[prop] as any as T[]);
}
}
};

View File

@ -8,3 +8,4 @@ export * from './dom';
export * from './decorators';
export * from './debug';
export * from './memoize';
export * from './sort';

View File

@ -14,8 +14,8 @@ export async function loadAndBundleSpec(specUrlOrObject: object | string): Promi
const config = new Config({});
const bundleOpts = {
config,
base: IS_BROWSER ? window.location.href : process.cwd()
}
base: IS_BROWSER ? window.location.href : process.cwd(),
};
if (IS_BROWSER) {
config.resolve.http.customFetch = global.fetch;
@ -24,13 +24,15 @@ export async function loadAndBundleSpec(specUrlOrObject: object | string): Promi
if (typeof specUrlOrObject === 'object' && specUrlOrObject !== null) {
bundleOpts['doc'] = {
source: { absoluteRef: '' } as Source,
parsed: specUrlOrObject
} as Document
parsed: specUrlOrObject,
} as Document;
} else {
bundleOpts['ref'] = specUrlOrObject;
}
const { bundle: { parsed } } = await bundle(bundleOpts);
const {
bundle: { parsed },
} = await bundle(bundleOpts);
return parsed.swagger !== undefined ? convertSwagger2OpenAPI(parsed) : parsed;
}

View File

@ -3,7 +3,7 @@ const SENTINEL = {};
export function memoize<T>(target: any, name: string, descriptor: TypedPropertyDescriptor<T>) {
if (typeof descriptor.value === 'function') {
return (_memoizeMethod(target, name, descriptor) as any) as TypedPropertyDescriptor<T>;
return _memoizeMethod(target, name, descriptor) as any as TypedPropertyDescriptor<T>;
} else if (typeof descriptor.get === 'function') {
return _memoizeGetter(target, name, descriptor) as TypedPropertyDescriptor<T>;
} else {

View File

@ -9,6 +9,8 @@ import {
OpenAPIMediaType,
OpenAPIParameter,
OpenAPIParameterStyle,
OpenAPIRequestBody,
OpenAPIResponse,
OpenAPISchema,
OpenAPIServer,
Referenced,
@ -113,7 +115,10 @@ export function detectType(schema: OpenAPISchema): string {
return 'any';
}
export function isPrimitiveType(schema: OpenAPISchema, type: string | string[] | undefined = schema.type) {
export function isPrimitiveType(
schema: OpenAPISchema,
type: string | string[] | undefined = schema.type,
) {
if (schema.oneOf !== undefined || schema.anyOf !== undefined) {
return false;
}
@ -122,9 +127,10 @@ export function isPrimitiveType(schema: OpenAPISchema, type: string | string[] |
const isArray = Array.isArray(type);
if (type === 'object' || (isArray && type?.includes('object'))) {
isPrimitive = schema.properties !== undefined
? Object.keys(schema.properties).length === 0
: schema.additionalProperties === undefined;
isPrimitive =
schema.properties !== undefined
? Object.keys(schema.properties).length === 0
: schema.additionalProperties === undefined;
}
if (schema.items !== undefined && (type === 'array' || (isArray && type?.includes('array')))) {
@ -385,7 +391,7 @@ export function isNamedDefinition(pointer?: string): boolean {
export function getDefinitionName(pointer?: string): string | undefined {
if (!pointer) return undefined;
const match = pointer.match(/^#\/components\/(schemas|pathItems)\/([^\/]+)$/);
return match === null ? undefined : match[1]
return match === null ? undefined : match[1];
}
function humanizeMultipleOfConstraint(multipleOf: number | undefined): string | undefined {
@ -424,6 +430,29 @@ function humanizeRangeConstraint(
return stringRange;
}
export function humanizeNumberRange(schema: OpenAPISchema): string | undefined {
const minimum =
typeof schema.exclusiveMinimum === 'number'
? Math.min(schema.exclusiveMinimum, schema.minimum ?? Infinity)
: schema.minimum;
const maximum =
typeof schema.exclusiveMaximum === 'number'
? Math.max(schema.exclusiveMaximum, schema.maximum ?? -Infinity)
: schema.maximum;
const exclusiveMinimum = typeof schema.exclusiveMinimum === 'number' || schema.exclusiveMinimum;
const exclusiveMaximum = typeof schema.exclusiveMaximum === 'number' || schema.exclusiveMaximum;
if (minimum !== undefined && maximum !== undefined) {
return `${exclusiveMinimum ? '( ' : '[ '}${minimum} .. ${maximum}${
exclusiveMaximum ? ' )' : ' ]'
}`;
} else if (maximum !== undefined) {
return `${exclusiveMaximum ? '< ' : '<= '}${maximum}`;
} else if (minimum !== undefined) {
return `${exclusiveMinimum ? '> ' : '>= '}${minimum}`;
}
}
export function humanizeConstraints(schema: OpenAPISchema): string[] {
const res: string[] = [];
@ -442,33 +471,7 @@ export function humanizeConstraints(schema: OpenAPISchema): string[] {
res.push(multipleOfConstraint);
}
let numberRange;
if (schema.minimum !== undefined && schema.maximum !== undefined) {
numberRange = schema.exclusiveMinimum ? '( ' : '[ ';
numberRange += schema.minimum;
numberRange += ' .. ';
numberRange += schema.maximum;
numberRange += schema.exclusiveMaximum ? ' )' : ' ]';
} else if (schema.maximum !== undefined) {
numberRange = schema.exclusiveMaximum ? '< ' : '<= ';
numberRange += schema.maximum;
} else if (schema.minimum !== undefined) {
numberRange = schema.exclusiveMinimum ? '> ' : '>= ';
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}]`
}
const numberRange = humanizeNumberRange(schema);
if (numberRange !== undefined) {
res.push(numberRange);
}
@ -598,10 +601,10 @@ export function setSecuritySchemePrefix(prefix: string) {
}
export const shortenHTTPVerb = verb =>
({
delete: 'del',
options: 'opts',
}[verb] || verb);
({
delete: 'del',
options: 'opts',
}[verb] || verb);
export function isRedocExtension(key: string): boolean {
const redocExtensions = {
@ -646,3 +649,33 @@ export function pluralizeType(displayType: string): string {
.map(type => type.replace(/^(string|object|number|integer|array|boolean)s?( ?.*)/, '$1s$2'))
.join(' or ');
}
export function getContentWithLegacyExamples(
info: OpenAPIRequestBody | OpenAPIResponse,
): { [mime: string]: OpenAPIMediaType } | undefined {
let mediaContent = info.content;
const xExamples = info['x-examples']; // converted from OAS2 body param
const xExample = info['x-example']; // converted from OAS2 body param
if (xExamples) {
mediaContent = { ...mediaContent };
for (const mime of Object.keys(xExamples)) {
const examples = xExamples[mime];
mediaContent[mime] = {
...mediaContent[mime],
examples,
};
}
} else if (xExample) {
mediaContent = { ...mediaContent };
for (const mime of Object.keys(xExample)) {
const example = xExample[mime];
mediaContent[mime] = {
...mediaContent[mime],
example,
};
}
}
return mediaContent;
}

21
src/utils/sort.ts Normal file
View File

@ -0,0 +1,21 @@
/**
* Function that returns a comparator for sorting objects by some specific key alphabetically.
*
* @param {String} property key of the object to sort, if starts from `-` - reverse
*/
export function alphabeticallyByProp<T>(property: string): (a: T, b: T) => number {
let sortOrder = 1;
if (property[0] === '-') {
sortOrder = -1;
property = property.substr(1);
}
return (a: T, b: T) => {
if (sortOrder == -1) {
return b[property].localeCompare(a[property]);
} else {
return a[property].localeCompare(b[property]);
}
};
}

View File

@ -32,10 +32,14 @@ const BANNER = `ReDoc - OpenAPI/Swagger-generated API Reference Documentation
Version: ${VERSION}
Repo: https://github.com/Redocly/redoc`;
export default (env: { standalone?: boolean } = {}) => ({
export default (env: { standalone?: boolean; browser?: boolean } = {}) => ({
entry: env.standalone ? ['./src/polyfills.ts', './src/standalone.tsx'] : './src/index.ts',
output: {
filename: env.standalone ? 'redoc.standalone.js' : 'redoc.lib.js',
filename: env.standalone
? 'redoc.standalone.js'
: env.browser
? 'redoc.browser.lib.js'
: 'redoc.lib.js',
path: path.join(__dirname, '/bundles'),
library: 'Redoc',
libraryTarget: 'umd',
@ -47,13 +51,13 @@ export default (env: { standalone?: boolean } = {}) => ({
fallback: {
path: require.resolve('path-browserify'),
http: false,
fs: false,
os: false,
}
fs: path.resolve(__dirname, 'src/empty.js'),
os: path.resolve(__dirname, 'src/empty.js'),
tty: path.resolve(__dirname, 'src/empty.js'),
},
},
performance: false,
// target: 'node',
externalsPresets: env.standalone ? {} : { node: true },
externalsPresets: env.standalone || env.browser ? {} : { node: true },
externals: env.standalone
? {
esprima: 'null',
@ -74,7 +78,7 @@ export default (env: { standalone?: boolean } = {}) => ({
rules: [
{
test: /\.(tsx?|[cm]?js)$/,
use: [getBabelLoader({useBuiltIns: !!env.standalone})],
use: [getBabelLoader({ useBuiltIns: !!env.standalone })],
exclude: {
and: [/node_modules/],
not: {