Merge branch 'Redocly:master' into master

This commit is contained in:
Depickere Sven 2022-01-27 11:05:41 +01:00 committed by GitHub
commit f43a030283
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
100 changed files with 7404 additions and 7344 deletions

View File

@ -17,7 +17,7 @@ module.exports = {
version: 'detect', version: 'detect',
}, },
}, },
plugins: ['@typescript-eslint', 'import'], plugins: ['react', 'react-hooks', '@typescript-eslint', 'import'],
rules: { rules: {
'@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off',
@ -31,6 +31,8 @@ module.exports = {
'@typescript-eslint/no-var-requires': 'off', '@typescript-eslint/no-var-requires': 'off',
'react/prop-types': 'off', 'react/prop-types': 'off',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
'import/no-extraneous-dependencies': 'error', 'import/no-extraneous-dependencies': 'error',
'import/no-internal-modules': [ 'import/no-internal-modules': [

View File

@ -5,7 +5,6 @@ on:
branches: branches:
- master - master
jobs: jobs:
bundle: bundle:
needs: [check-version-cli] needs: [check-version-cli]
@ -99,7 +98,7 @@ jobs:
steps: steps:
- uses: actions/setup-node@v1 - uses: actions/setup-node@v1
with: with:
node-version: "14.x" node-version: '14.x'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Download cli bundled artifact - name: Download cli bundled artifact

View File

@ -76,7 +76,7 @@ jobs:
steps: steps:
- uses: actions/setup-node@v1 - uses: actions/setup-node@v1
with: with:
node-version: "14.x" node-version: '14.x'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Download bundled artifacts - name: Download bundled artifacts

1
.gitignore vendored
View File

@ -37,3 +37,4 @@ stats.json
yarn.lock yarn.lock
.idea .idea
.vscode .vscode
.eslintcache

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,64 @@
# [2.0.0-rc.62](https://github.com/Redocly/redoc/compare/v2.0.0-rc.61...v2.0.0-rc.62) (2022-01-26)
### Bug Fixes
* fix field expand does not work ([#1875](https://github.com/Redocly/redoc/issues/1875))
# [2.0.0-rc.61](https://github.com/Redocly/redoc/compare/v2.0.0-rc.60...v2.0.0-rc.61) (2022-01-26)
### Bug Fixes
* fix crash in redoc-cli after migrating to esbuild ([#1872](https://github.com/Redocly/redoc/issues/1872))
# [2.0.0-rc.60](https://github.com/Redocly/redoc/compare/v2.0.0-rc.59...v2.0.0-rc.60) (2022-01-25)
### Bug Fixes
* add schema expansion level ([#1868](https://github.com/Redocly/redoc/issues/1868)) ([250d53a](https://github.com/Redocly/redoc/commit/250d53a59fb4bf881875ba466c5a7f3b55d80007))
* attachHeadingsDescriptions match headings incorrectly ([#1845](https://github.com/Redocly/redoc/issues/1845)) ([ea8573d](https://github.com/Redocly/redoc/commit/ea8573dbd78439be50aa2b38f1c83658c16783e3))
* definition name util ([#1865](https://github.com/Redocly/redoc/issues/1865)) ([95a7347](https://github.com/Redocly/redoc/commit/95a734793158d4749e98ee4a7e90e70713a04ced))
* No maxLength label is displayed for arrays of items [#1701](https://github.com/Redocly/redoc/issues/1701) ([#1765](https://github.com/Redocly/redoc/issues/1765)) ([6c7685e](https://github.com/Redocly/redoc/commit/6c7685e5fa04314328a445d7077600692c49489c))
* Response objects couldn't open ([#1867](https://github.com/Redocly/redoc/issues/1867)) ([18f943d](https://github.com/Redocly/redoc/commit/18f943d2b5668f1552d212dee1c3a2ed59054095))
* writeOnly params displaying in webhook ([#1866](https://github.com/Redocly/redoc/issues/1866)) ([5694913](https://github.com/Redocly/redoc/commit/5694913e71f0e8c3a5d9393f1b4ae92534127841))
### Features
* **#1251:** Add file selector to demo application ([#1859](https://github.com/Redocly/redoc/issues/1859)) ([b74dcde](https://github.com/Redocly/redoc/commit/b74dcde42b45ebe5ae617f1ec3cfea2ea1aff922)), closes [#1251](https://github.com/Redocly/redoc/issues/1251) [#1251](https://github.com/Redocly/redoc/issues/1251) [#1251](https://github.com/Redocly/redoc/issues/1251)
* redoc-cli add host option ([#1598](https://github.com/Redocly/redoc/issues/1598)) ([fb104e6](https://github.com/Redocly/redoc/commit/fb104e696618b0b81439da134887830a0f2439ea))
* support examples in object schema ([#1832](https://github.com/Redocly/redoc/issues/1832)) ([c986f0e](https://github.com/Redocly/redoc/commit/c986f0ef1a38bc1e61cae70830d84de03b684b89))
# [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) # [2.0.0-rc.57](https://github.com/Redocly/redoc/compare/v2.0.0-rc.56...v2.0.0-rc.57) (2021-10-11)

View File

@ -3,7 +3,7 @@
# Generate interactive API documentation from OpenAPI definitions # 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/) [![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> </div>
@ -183,6 +183,7 @@ IE support for Redoc.
For more information on Redoc's commmand-line interface, refer to For more information on Redoc's commmand-line interface, refer to
[**Using the Redoc CLI**](https://redoc.ly/docs/redoc/quickstart/cli/). [**Using the Redoc CLI**](https://redoc.ly/docs/redoc/quickstart/cli/).
## Configuration ## Configuration
### Security Definition location ### 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. * `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). * `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!** * `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 ### `<redoc>` theme object
* `spacing` * `spacing`

View File

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

178
cli/npm-shrinkwrap.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "redoc-cli", "name": "redoc-cli",
"version": "0.13.0", "version": "0.13.6",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "redoc-cli", "name": "redoc-cli",
"version": "0.13.0", "version": "0.13.6",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"chokidar": "^3.5.1", "chokidar": "^3.5.1",
@ -17,7 +17,7 @@
"node-libs-browser": "^2.2.1", "node-libs-browser": "^2.2.1",
"react": "^17.0.1", "react": "^17.0.1",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"redoc": "2.0.0-rc.57", "redoc": "2.0.0-rc.61",
"styled-components": "^5.3.0", "styled-components": "^5.3.0",
"yargs": "^17.0.1" "yargs": "^17.0.1"
}, },
@ -267,9 +267,9 @@
} }
}, },
"node_modules/@types/json-schema": { "node_modules/@types/json-schema": {
"version": "7.0.7", "version": "7.0.9",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
"integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==" "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ=="
}, },
"node_modules/@types/mkdirp": { "node_modules/@types/mkdirp": {
"version": "1.0.1", "version": "1.0.1",
@ -406,6 +406,14 @@
} }
] ]
}, },
"node_modules/big.js": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
"integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
"engines": {
"node": "*"
}
},
"node_modules/binary-extensions": { "node_modules/binary-extensions": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@ -834,6 +842,14 @@
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
}, },
"node_modules/emojis-list": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
"engines": {
"node": ">= 4"
}
},
"node_modules/es6-promise": { "node_modules/es6-promise": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz",
@ -921,9 +937,7 @@
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true, "hasInstallScript": true,
"optional": true, "optional": true,
"os": [ "os": ["darwin"],
"darwin"
],
"engines": { "engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0" "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
} }
@ -1126,6 +1140,20 @@
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="
}, },
"node_modules/isomorphic-style-loader": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/isomorphic-style-loader/-/isomorphic-style-loader-5.3.2.tgz",
"integrity": "sha512-5mwHrN2xK5zsKBxSUYF7iDhoU9Kpcpfgn0lFOP0SKk3aKwkl26zi6kh+KDrekjlLzNbYsFnn8o1yWaB3OflVXQ==",
"dependencies": {
"hoist-non-react-statics": "^3.0.0",
"loader-utils": "^1.2.3",
"prop-types": "^15.7.2"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0",
"react-dom": "^16.8.0 || ^17.0.0"
}
},
"node_modules/js-levenshtein": { "node_modules/js-levenshtein": {
"version": "1.1.6", "version": "1.1.6",
"resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
@ -1175,6 +1203,30 @@
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
}, },
"node_modules/json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"dependencies": {
"minimist": "^1.2.0"
},
"bin": {
"json5": "lib/cli.js"
}
},
"node_modules/loader-utils": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
"integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
"dependencies": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^1.0.1"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/lodash": { "node_modules/lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@ -1207,14 +1259,14 @@
"integrity": "sha1-GA8fnr74sOY45BZq1S24eb6y/8U=" "integrity": "sha1-GA8fnr74sOY45BZq1S24eb6y/8U="
}, },
"node_modules/marked": { "node_modules/marked": {
"version": "0.7.0", "version": "4.0.10",
"resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz", "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.10.tgz",
"integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==", "integrity": "sha512-+QvuFj0nGgO970fySghXGmuw+Fd0gD2x3+MqCWLIPf5oxdv1Ka6b2q+z9RP01P/IaKPMEramy+7cNy/Lw8c3hw==",
"bin": { "bin": {
"marked": "bin/marked" "marked": "bin/marked.js"
}, },
"engines": { "engines": {
"node": ">=0.10.0" "node": ">= 12"
} }
}, },
"node_modules/md5.js": { "node_modules/md5.js": {
@ -1227,11 +1279,6 @@
"safe-buffer": "^5.1.2" "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": { "node_modules/miller-rabin": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz",
@ -1489,9 +1536,9 @@
} }
}, },
"node_modules/openapi-sampler": { "node_modules/openapi-sampler": {
"version": "1.0.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.0.1.tgz", "resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.1.1.tgz",
"integrity": "sha512-qBjxkSLJV183zTTs4fgxtU/iWSLUUu2aH2+5ddWkNhV7p8CSe/mnAgoLkEbMfHtel6yr9NF+vjUWqfM+iiwORQ==", "integrity": "sha512-WAFsl5SPYuhQwaMTDFOcKhnEY1G1rmamrMiPmJdqwfl1lr81g63/befcsN9BNi0w5/R0L+hfcUj13PANEBeLgg==",
"dependencies": { "dependencies": {
"@types/json-schema": "^7.0.7", "@types/json-schema": "^7.0.7",
"json-pointer": "^0.6.1" "json-pointer": "^0.6.1"
@ -1747,24 +1794,23 @@
} }
}, },
"node_modules/redoc": { "node_modules/redoc": {
"version": "2.0.0-rc.57", "version": "2.0.0-rc.61",
"resolved": "https://registry.npmjs.org/redoc/-/redoc-2.0.0-rc.57.tgz", "resolved": "https://registry.npmjs.org/redoc/-/redoc-2.0.0-rc.61.tgz",
"integrity": "sha512-f8XIqvZF1agphq6xmOU9jTDVNDFHJt3MzDq1lUgZojb/7YY4eqLyDi6er/yCWYkY9DuB+v2jHCOn5UUbMuKAfg==", "integrity": "sha512-fZLz8JI8zAHiYcRUHbiq6R7sRlR6EgMXrFdpD9GE0Fmp5laScEC2XOQjsPp0uYQgzvhKQSOg/7WyVrZ6GHualQ==",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.14.0",
"@redocly/openapi-core": "^1.0.0-beta.54", "@redocly/openapi-core": "^1.0.0-beta.54",
"@redocly/react-dropdown-aria": "^2.0.11", "@redocly/react-dropdown-aria": "^2.0.11",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"decko": "^1.2.0", "decko": "^1.2.0",
"dompurify": "^2.2.8", "dompurify": "^2.2.8",
"eventemitter3": "^4.0.7", "eventemitter3": "^4.0.7",
"isomorphic-style-loader": "^5.3.2",
"json-pointer": "^0.6.1", "json-pointer": "^0.6.1",
"lunr": "^2.3.9", "lunr": "^2.3.9",
"mark.js": "^8.11.1", "mark.js": "^8.11.1",
"marked": "^0.7.0", "marked": "^4.0.10",
"memoize-one": "^5.2.1",
"mobx-react": "^7.2.0", "mobx-react": "^7.2.0",
"openapi-sampler": "^1.0.1", "openapi-sampler": "^1.1.1",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"perfect-scrollbar": "^1.5.1", "perfect-scrollbar": "^1.5.1",
"polished": "^4.1.3", "polished": "^4.1.3",
@ -2512,9 +2558,9 @@
} }
}, },
"@types/json-schema": { "@types/json-schema": {
"version": "7.0.7", "version": "7.0.9",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
"integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==" "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ=="
}, },
"@types/mkdirp": { "@types/mkdirp": {
"version": "1.0.1", "version": "1.0.1",
@ -2629,6 +2675,11 @@
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
}, },
"big.js": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
"integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ=="
},
"binary-extensions": { "binary-extensions": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@ -3024,6 +3075,11 @@
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
}, },
"emojis-list": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q=="
},
"es6-promise": { "es6-promise": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz",
@ -3239,6 +3295,16 @@
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="
}, },
"isomorphic-style-loader": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/isomorphic-style-loader/-/isomorphic-style-loader-5.3.2.tgz",
"integrity": "sha512-5mwHrN2xK5zsKBxSUYF7iDhoU9Kpcpfgn0lFOP0SKk3aKwkl26zi6kh+KDrekjlLzNbYsFnn8o1yWaB3OflVXQ==",
"requires": {
"hoist-non-react-statics": "^3.0.0",
"loader-utils": "^1.2.3",
"prop-types": "^15.7.2"
}
},
"js-levenshtein": { "js-levenshtein": {
"version": "1.1.6", "version": "1.1.6",
"resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
@ -3276,6 +3342,24 @@
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
}, },
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"requires": {
"minimist": "^1.2.0"
}
},
"loader-utils": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
"integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^1.0.1"
}
},
"lodash": { "lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@ -3305,9 +3389,9 @@
"integrity": "sha1-GA8fnr74sOY45BZq1S24eb6y/8U=" "integrity": "sha1-GA8fnr74sOY45BZq1S24eb6y/8U="
}, },
"marked": { "marked": {
"version": "0.7.0", "version": "4.0.10",
"resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz", "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.10.tgz",
"integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==" "integrity": "sha512-+QvuFj0nGgO970fySghXGmuw+Fd0gD2x3+MqCWLIPf5oxdv1Ka6b2q+z9RP01P/IaKPMEramy+7cNy/Lw8c3hw=="
}, },
"md5.js": { "md5.js": {
"version": "1.3.5", "version": "1.3.5",
@ -3319,11 +3403,6 @@
"safe-buffer": "^5.1.2" "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": { "miller-rabin": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz",
@ -3509,9 +3588,9 @@
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
}, },
"openapi-sampler": { "openapi-sampler": {
"version": "1.0.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.0.1.tgz", "resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.1.1.tgz",
"integrity": "sha512-qBjxkSLJV183zTTs4fgxtU/iWSLUUu2aH2+5ddWkNhV7p8CSe/mnAgoLkEbMfHtel6yr9NF+vjUWqfM+iiwORQ==", "integrity": "sha512-WAFsl5SPYuhQwaMTDFOcKhnEY1G1rmamrMiPmJdqwfl1lr81g63/befcsN9BNi0w5/R0L+hfcUj13PANEBeLgg==",
"requires": { "requires": {
"@types/json-schema": "^7.0.7", "@types/json-schema": "^7.0.7",
"json-pointer": "^0.6.1" "json-pointer": "^0.6.1"
@ -3740,24 +3819,23 @@
} }
}, },
"redoc": { "redoc": {
"version": "2.0.0-rc.57", "version": "2.0.0-rc.61",
"resolved": "https://registry.npmjs.org/redoc/-/redoc-2.0.0-rc.57.tgz", "resolved": "https://registry.npmjs.org/redoc/-/redoc-2.0.0-rc.61.tgz",
"integrity": "sha512-f8XIqvZF1agphq6xmOU9jTDVNDFHJt3MzDq1lUgZojb/7YY4eqLyDi6er/yCWYkY9DuB+v2jHCOn5UUbMuKAfg==", "integrity": "sha512-fZLz8JI8zAHiYcRUHbiq6R7sRlR6EgMXrFdpD9GE0Fmp5laScEC2XOQjsPp0uYQgzvhKQSOg/7WyVrZ6GHualQ==",
"requires": { "requires": {
"@babel/runtime": "^7.14.0",
"@redocly/openapi-core": "^1.0.0-beta.54", "@redocly/openapi-core": "^1.0.0-beta.54",
"@redocly/react-dropdown-aria": "^2.0.11", "@redocly/react-dropdown-aria": "^2.0.11",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"decko": "^1.2.0", "decko": "^1.2.0",
"dompurify": "^2.2.8", "dompurify": "^2.2.8",
"eventemitter3": "^4.0.7", "eventemitter3": "^4.0.7",
"isomorphic-style-loader": "^5.3.2",
"json-pointer": "^0.6.1", "json-pointer": "^0.6.1",
"lunr": "^2.3.9", "lunr": "^2.3.9",
"mark.js": "^8.11.1", "mark.js": "^8.11.1",
"marked": "^0.7.0", "marked": "^4.0.10",
"memoize-one": "^5.2.1",
"mobx-react": "^7.2.0", "mobx-react": "^7.2.0",
"openapi-sampler": "^1.0.1", "openapi-sampler": "^1.1.1",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"perfect-scrollbar": "^1.5.1", "perfect-scrollbar": "^1.5.1",
"polished": "^4.1.3", "polished": "^4.1.3",

View File

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

View File

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

View File

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

2
custom.d.ts vendored
View File

@ -23,3 +23,5 @@ declare var reactHotLoaderGlobal: any;
interface Element { interface Element {
scrollIntoViewIfNeeded(centerIfNeeded?: boolean): void; scrollIntoViewIfNeeded(centerIfNeeded?: boolean): void;
} }
type GenericObject = Record<string, any>;

View File

@ -0,0 +1,52 @@
import * as yaml from 'js-yaml';
import * as React from 'react';
import { ChangeEvent, RefObject, useRef } from 'react';
import styled from '../../src/styled-components';
const Button = styled.button`
background-color: #fff;
color: #333;
padding: 2px 10px;
touch-action: manipulation;
cursor: pointer;
user-select: none;
border: 1px solid #ccc;
font-size: 16px;
height: 28px;
box-sizing: border-box;
vertical-align: middle;
line-height: 1;
outline: none;
white-space: nowrap;
@media (max-width: 699px) {
display: none;
}
`;
function FileInput(props: { onUpload }) {
const hiddenFileInput: RefObject<HTMLInputElement> = useRef<HTMLInputElement>(null);
const handleClick = () => {
if (hiddenFileInput && hiddenFileInput.current) {
hiddenFileInput.current.click();
}
};
const uploadFile = (event: ChangeEvent<HTMLInputElement>) => {
const file = (event.target as HTMLInputElement).files![0];
const reader = new FileReader();
reader.onload = () => {
props.onUpload(yaml.load(reader.result));
};
reader.readAsText(file);
};
return (
<span>
<Button onClick={handleClick}>Upload a file</Button>
<input type="file" style={{ display: 'none' }} onChange={uploadFile} ref={hiddenFileInput} />
</span>
);
}
export default FileInput;

View File

@ -4,6 +4,7 @@ import styled from 'styled-components';
import { resolve as urlResolve } from 'url'; import { resolve as urlResolve } from 'url';
import { RedocStandalone } from '../src'; import { RedocStandalone } from '../src';
import ComboBox from './ComboBox'; import ComboBox from './ComboBox';
import FileInput from './components/FileInput';
const DEFAULT_SPEC = 'openapi.yaml'; const DEFAULT_SPEC = 'openapi.yaml';
const NEW_VERSION_SPEC = 'openapi-3-1.yaml'; const NEW_VERSION_SPEC = 'openapi-3-1.yaml';
@ -22,7 +23,7 @@ const demos = [
class DemoApp extends React.Component< class DemoApp extends React.Component<
{}, {},
{ specUrl: string; dropdownOpen: boolean; cors: boolean } { spec: object | undefined; specUrl: string; dropdownOpen: boolean; cors: boolean }
> { > {
constructor(props) { constructor(props) {
super(props); super(props);
@ -40,15 +41,24 @@ class DemoApp extends React.Component<
} }
this.state = { this.state = {
spec: undefined,
specUrl: url, specUrl: url,
dropdownOpen: false, dropdownOpen: false,
cors, cors,
}; };
} }
handleUploadFile = (spec: object) => {
this.setState({
spec,
specUrl: '',
});
};
handleChange = (url: string) => { handleChange = (url: string) => {
if (url === NEW_VERSION_SPEC) { if (url === NEW_VERSION_SPEC) {
this.setState({ cors: false }) this.setState({ cors: false });
0;
} }
this.setState({ this.setState({
specUrl: url, specUrl: url,
@ -90,6 +100,7 @@ class DemoApp extends React.Component<
/> />
</a> </a>
<ControlsContainer> <ControlsContainer>
<FileInput onUpload={this.handleUploadFile} />
<ComboBox <ComboBox
placeholder={'URL to a spec to try'} placeholder={'URL to a spec to try'}
options={demos} options={demos}
@ -110,6 +121,7 @@ class DemoApp extends React.Component<
/> />
</Heading> </Heading>
<RedocStandalone <RedocStandalone
spec={this.state.spec}
specUrl={proxiedUrl} specUrl={proxiedUrl}
options={{ scrollYOffset: 'nav', untrustedSpec: true }} options={{ scrollYOffset: 'nav', untrustedSpec: true }}
/> />

View File

@ -3,7 +3,7 @@ import ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
import * as HtmlWebpackPlugin from 'html-webpack-plugin'; import * as HtmlWebpackPlugin from 'html-webpack-plugin';
import { resolve } from 'path'; import { resolve } from 'path';
import * as webpack from 'webpack'; import * as webpack from 'webpack';
import { getBabelLoader, webpackIgnore } from '../config/webpack-utils'; import { webpackIgnore } from '../config/webpack-utils';
const VERSION = JSON.stringify(require('../package.json').version); const VERSION = JSON.stringify(require('../package.json').version);
const REVISION = JSON.stringify( const REVISION = JSON.stringify(
@ -33,18 +33,20 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
}, },
devServer: { devServer: {
contentBase: __dirname, static: __dirname,
watchContentBase: true,
port: 9090, port: 9090,
disableHostCheck: true,
stats: 'minimal',
hot: true, hot: true,
historyApiFallback: true,
open: true,
},
stats: {
children: true,
}, },
resolve: { resolve: {
extensions: ['.ts', '.tsx', '.js', '.json'], extensions: ['.ts', '.tsx', '.js', '.json'],
fallback: { fallback: {
path: require.resolve('path-browserify'), path: require.resolve('path-browserify'),
buffer: require.resolve('buffer'),
http: false, http: false,
fs: false, fs: false,
os: false, os: false,
@ -71,33 +73,28 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
rules: [ rules: [
{ test: [/\.eot$/, /\.gif$/, /\.woff$/, /\.svg$/, /\.ttf$/], use: 'null-loader' }, { test: [/\.eot$/, /\.gif$/, /\.woff$/, /\.svg$/, /\.ttf$/], use: 'null-loader' },
{ {
test: /\.tsx?$/, test: /\.(tsx?|[cm]?js)$/,
use: [getBabelLoader({useBuiltIns: true, hot: true} )], loader: 'esbuild-loader',
exclude: { options: {
and: [/node_modules/], loader: 'tsx',
not: { target: 'es2015',
or: [ tsconfigRaw: require('../tsconfig.json'),
/swagger2openapi/,
/reftools/,
/openapi-sampler/,
/mobx/,
/oas-resolver/,
/oas-kit-common/,
/oas-schema-walker/,
/\@redocly\/openapi-core/,
/colorette/,
],
},
}, },
exclude: [/node_modules/],
}, },
{ {
test: /\.css$/, test: /\.css$/,
use: { use: [
loader: 'css-loader', 'isomorphic-style-loader',
'css-loader',
{
loader: 'esbuild-loader',
options: { options: {
sourceMap: true, loader: 'css',
minify: true,
}, },
}, },
],
}, },
], ],
}, },
@ -118,6 +115,9 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
? 'benchmark/index.html' ? 'benchmark/index.html'
: 'demo/index.html', : 'demo/index.html',
}), }),
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
}),
new ForkTsCheckerWebpackPlugin({ logger: { infrastructure: 'silent', issues: 'console' } }), new ForkTsCheckerWebpackPlugin({ logger: { infrastructure: 'silent', issues: 'console' } }),
webpackIgnore(/js-yaml\/dumper\.js$/), webpackIgnore(/js-yaml\/dumper\.js$/),
webpackIgnore(/json-schema-ref-parser\/lib\/dereference\.js/), webpackIgnore(/json-schema-ref-parser\/lib\/dereference\.js/),

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

@ -4,9 +4,7 @@ describe('Menu', () => {
}); });
it('should have valid items count', () => { it('should have valid items count', () => {
cy.get('.menu-content') cy.get('.menu-content').find('li').should('have.length', 34);
.find('li')
.should('have.length', 34);
}); });
it('should sync active menu items while scroll', () => { it('should sync active menu items while scroll', () => {
@ -35,9 +33,7 @@ describe('Menu', () => {
.should('have.text', 'Add a new pet to the store') .should('have.text', 'Add a new pet to the store')
.should('be.visible'); .should('be.visible');
cy.contains('h1', 'Swagger Petstore') cy.contains('h1', 'Swagger Petstore').scrollIntoView().wait(100);
.scrollIntoView()
.wait(100)
cy.contains('h1', 'Introduction') cy.contains('h1', 'Introduction')
.scrollIntoView() .scrollIntoView()
@ -59,10 +55,25 @@ describe('Menu', () => {
it('should deactivate tag when other is activated', () => { it('should deactivate tag when other is activated', () => {
const petItem = () => cy.contains('[role=menuitem].-depth1', 'pet'); const petItem = () => cy.contains('[role=menuitem].-depth1', 'pet');
petItem() petItem().click({ force: true }).should('have.class', 'active');
.click({ force: true })
.should('have.class', 'active');
cy.contains('[role=menuitem].-depth1', 'store').click({ force: true }); cy.contains('[role=menuitem].-depth1', 'store').click({ force: true });
petItem().should('not.have.class', 'active'); petItem().should('not.have.class', 'active');
}); });
it('should be able to open a response object to see more details', () => {
cy.contains('h2', 'Find pet by ID')
.scrollIntoView()
.wait(100)
.parent()
.find('div h3')
.should('have.text', 'Responses')
.parent()
.find('div:first button')
.click()
.should('have.attr', 'aria-expanded', 'true')
.parent()
.find('div h5')
.then($h5 => $h5[0].firstChild!.nodeValue!.trim())
.should('eq', 'Response Schema:');
});
}); });

View File

@ -58,5 +58,5 @@ describe('Search', () => {
getSearchResults().should('not.exist'); getSearchResults().should('not.exist');
getSearchInput().type('xzss', { force: true }); getSearchInput().type('xzss', { force: true });
getSearchResults().should('exist').should('contain', 'No results found'); getSearchResults().should('exist').should('contain', 'No results found');
}) });
}); });

9361
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "redoc", "name": "redoc",
"version": "2.0.0-rc.57", "version": "2.0.0-rc.62",
"description": "ReDoc", "description": "ReDoc",
"repository": { "repository": {
"type": "git", "type": "git",
@ -27,6 +27,7 @@
"React.js" "React.js"
], ],
"main": "bundles/redoc.lib.js", "main": "bundles/redoc.lib.js",
"browser": "bundles/redoc.browser.lib.js",
"types": "typings/index.d.ts", "types": "typings/index.d.ts",
"scripts": { "scripts": {
"start": "webpack serve --mode=development --env playground --hot --config demo/webpack.config.ts", "start": "webpack serve --mode=development --env playground --hot --config demo/webpack.config.ts",
@ -36,18 +37,19 @@
"unit": "jest --coverage", "unit": "jest --coverage",
"e2e": "cypress run", "e2e": "cypress run",
"e2e-ci": "cypress run --record", "e2e-ci": "cypress run --record",
"bundlesize": "bundlesize", "bundlesize": "size-limit",
"ts-check": "tsc --noEmit --skipLibCheck", "ts-check": "tsc --noEmit --skipLibCheck",
"cy:open": "cypress open", "cy:open": "cypress open",
"bundle:clean": "rimraf bundles", "bundle:clean": "rimraf bundles",
"bundle:standalone": "webpack --env production --env standalone --mode=production", "bundle:standalone": "webpack --env production --env standalone --mode=production",
"bundle:lib": "webpack --mode=production && npm run declarations", "bundle:lib": "webpack --mode=production && npm run declarations",
"bundle": "npm run bundle:clean && npm run bundle:lib && npm run bundle:standalone", "bundle: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/", "declarations": "tsc --emitDeclarationOnly -p tsconfig.lib.json && cp -R src/types typings/",
"stats": "webpack --env production --env standalone --json --profile --mode=production > stats.json", "stats": "webpack --env production --env standalone --json --profile --mode=production > stats.json",
"prettier": "prettier --write \"cli/index.ts\" \"src/**/*.{ts,tsx}\"", "prettier": "prettier --write \"cli/index.ts\" \"src/**/*.{ts,tsx}\"",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 1", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 1",
"lint": "eslint 'src/**/*.{js,ts,tsx}'", "lint": "eslint 'src/**/*.{js,ts,tsx}' --cache",
"benchmark": "node ./benchmark/benchmark.js", "benchmark": "node ./benchmark/benchmark.js",
"start:demo": "webpack serve --hot --config demo/webpack.config.ts --mode=development", "start:demo": "webpack serve --hot --config demo/webpack.config.ts --mode=development",
"compile:cli": "tsc custom.d.ts cli/index.ts --target es6 --module commonjs --types yargs", "compile:cli": "tsc custom.d.ts cli/index.ts --target es6 --module commonjs --types yargs",
@ -55,34 +57,24 @@
"build:demo": "webpack --mode=production --config demo/webpack.config.ts", "build:demo": "webpack --mode=production --config demo/webpack.config.ts",
"deploy:demo": "aws s3 sync demo/dist s3://production-redoc-demo --acl=public-read", "deploy:demo": "aws s3 sync demo/dist s3://production-redoc-demo --acl=public-read",
"license-check": "license-checker --production --onlyAllow 'MIT;ISC;Apache-2.0;BSD;BSD-2-Clause;BSD-3-Clause' --summary", "license-check": "license-checker --production --onlyAllow 'MIT;ISC;Apache-2.0;BSD;BSD-2-Clause;BSD-3-Clause' --summary",
"docker:build": "docker build -f config/docker/Dockerfile -t redoc ." "docker:build": "docker build -f config/docker/Dockerfile -t redoc .",
"prepare": "husky install",
"pre-commit": "pretty-quick --staged && npm run lint"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.14.3",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/plugin-proposal-decorators": "^7.14.2",
"@babel/plugin-proposal-object-rest-spread": "^7.14.4",
"@babel/plugin-syntax-decorators": "^7.12.13",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-jsx": "^7.10.4",
"@babel/plugin-syntax-typescript": "^7.10.4",
"@babel/plugin-transform-runtime": "^7.14.3",
"@babel/preset-env": "^7.14.4",
"@babel/preset-react": "^7.13.13",
"@babel/preset-typescript": "^7.13.0",
"@cypress/webpack-preprocessor": "^5.9.0", "@cypress/webpack-preprocessor": "^5.9.0",
"@hot-loader/react-dom": "^17.0.1", "@hot-loader/react-dom": "^17.0.1",
"@size-limit/preset-app": "^7.0.4",
"@types/chai": "^4.2.18", "@types/chai": "^4.2.18",
"@types/dompurify": "^2.2.2", "@types/dompurify": "^2.2.2",
"@types/enzyme": "^3.10.5", "@types/enzyme": "^3.10.5",
"@types/enzyme-to-json": "^1.5.3", "@types/enzyme-to-json": "^1.5.3",
"@types/jest": "^26.0.23", "@types/jest": "^26.0.23",
"@types/json-pointer": "^1.0.30", "@types/json-pointer": "^1.0.30",
"@types/lodash": "^4.14.170",
"@types/lunr": "^2.3.3", "@types/lunr": "^2.3.3",
"@types/node": "^15.6.1",
"@types/mark.js": "^8.11.5", "@types/mark.js": "^8.11.5",
"@types/marked": "^1.1.0", "@types/marked": "^4.0.1",
"@types/node": "^15.6.1",
"@types/prismjs": "^1.16.5", "@types/prismjs": "^1.16.5",
"@types/prop-types": "^15.7.3", "@types/prop-types": "^15.7.3",
"@types/react": "^17.0.8", "@types/react": "^17.0.8",
@ -96,10 +88,7 @@
"@typescript-eslint/eslint-plugin": "^4.26.0", "@typescript-eslint/eslint-plugin": "^4.26.0",
"@typescript-eslint/parser": "^4.26.0", "@typescript-eslint/parser": "^4.26.0",
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.1", "@wojtekmaj/enzyme-adapter-react-17": "^0.6.1",
"babel-loader": "^8.2.2",
"babel-plugin-styled-components": "^1.12.0",
"beautify-benchmark": "^0.2.4", "beautify-benchmark": "^0.2.4",
"bundlesize": "^0.18.1",
"conventional-changelog-cli": "^2.0.34", "conventional-changelog-cli": "^2.0.34",
"copy-webpack-plugin": "^9.0.0", "copy-webpack-plugin": "^9.0.0",
"core-js": "^3.13.1", "core-js": "^3.13.1",
@ -108,34 +97,38 @@
"cypress": "^7.4.0", "cypress": "^7.4.0",
"enzyme": "^3.11.0", "enzyme": "^3.11.0",
"enzyme-to-json": "^3.6.2", "enzyme-to-json": "^3.6.2",
"esbuild-loader": "^2.18.0",
"eslint": "^7.27.0", "eslint": "^7.27.0",
"eslint-plugin-import": "^2.23.4", "eslint-plugin-import": "^2.23.4",
"eslint-plugin-react": "^7.24.0", "eslint-plugin-react": "^7.25.1",
"eslint-plugin-react-hooks": "^4.2.0",
"fork-ts-checker-webpack-plugin": "^6.2.10", "fork-ts-checker-webpack-plugin": "^6.2.10",
"html-webpack-plugin": "^5.3.1", "html-webpack-plugin": "^5.3.1",
"husky": "^7.0.0",
"jest": "^27.0.3", "jest": "^27.0.3",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"license-checker": "^25.0.1", "license-checker": "^25.0.1",
"lodash": "^4.17.21", "lodash.noop": "^3.0.1",
"mobx": "^6.3.2", "mobx": "^6.3.2",
"prettier": "^2.3.0", "prettier": "^2.3.2",
"pretty-quick": "^3.0.0",
"raf": "^3.4.1", "raf": "^3.4.1",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-hot-loader": "^4.13.0", "react-hot-loader": "^4.13.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"shelljs": "^0.8.4", "shelljs": "^0.8.4",
"style-loader": "^2.0.0", "size-limit": "^7.0.4",
"styled-components": "^5.3.0", "styled-components": "^5.3.0",
"ts-jest": "^27.0.2", "ts-jest": "^27.0.2",
"ts-loader": "^8.0.1", "ts-loader": "^9.2.6",
"ts-node": "^10.0.0", "ts-node": "^10.0.0",
"typescript": "~4.1.0", "typescript": "~4.1.0",
"unfetch": "^4.2.0", "unfetch": "^4.2.0",
"url-polyfill": "^1.1.12", "url-polyfill": "^1.1.12",
"webpack": "^5.38.1", "webpack": "^5.38.1",
"webpack-cli": "^4.7.2", "webpack-cli": "^4.7.2",
"webpack-dev-server": "^3.11.2", "webpack-dev-server": "^4.6.0",
"webpack-node-externals": "^3.0.0", "webpack-node-externals": "^3.0.0",
"workerize-loader": "github:redocly/workerize-loader#webpack-5-dist" "workerize-loader": "github:redocly/workerize-loader#webpack-5-dist"
}, },
@ -147,20 +140,19 @@
"styled-components": "^4.1.1 || ^5.1.1" "styled-components": "^4.1.1 || ^5.1.1"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.14.0",
"@redocly/openapi-core": "^1.0.0-beta.54", "@redocly/openapi-core": "^1.0.0-beta.54",
"@redocly/react-dropdown-aria": "^2.0.11", "@redocly/react-dropdown-aria": "^2.0.11",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"decko": "^1.2.0", "decko": "^1.2.0",
"dompurify": "^2.2.8", "dompurify": "^2.2.8",
"eventemitter3": "^4.0.7", "eventemitter3": "^4.0.7",
"isomorphic-style-loader": "^5.3.2",
"json-pointer": "^0.6.1", "json-pointer": "^0.6.1",
"lunr": "^2.3.9", "lunr": "^2.3.9",
"mark.js": "^8.11.1", "mark.js": "^8.11.1",
"marked": "^0.7.0", "marked": "^4.0.10",
"memoize-one": "^5.2.1",
"mobx-react": "^7.2.0", "mobx-react": "^7.2.0",
"openapi-sampler": "^1.0.1", "openapi-sampler": "^1.1.1",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"perfect-scrollbar": "^1.5.1", "perfect-scrollbar": "^1.5.1",
"polished": "^4.1.3", "polished": "^4.1.3",
@ -172,10 +164,10 @@
"swagger2openapi": "^7.0.6", "swagger2openapi": "^7.0.6",
"url-template": "^2.0.8" "url-template": "^2.0.8"
}, },
"bundlesize": [ "size-limit": [
{ {
"path": "./bundles/redoc.standalone.js", "path": "./bundles/redoc.standalone.js",
"maxSize": "350 kB" "limit": "350 kB"
} }
], ],
"jest": { "jest": {
@ -208,6 +200,6 @@
"singleQuote": true, "singleQuote": true,
"trailingComma": "all", "trailingComma": "all",
"printWidth": 100, "printWidth": 100,
"parser": "typescript" "arrowParens": "avoid"
} }
} }

View File

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

View File

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

View File

@ -96,7 +96,6 @@ export const ConstraintItem = styled(FieldLabel)`
margin: 0 ${theme.spacing.unit}px; margin: 0 ${theme.spacing.unit}px;
padding: 0 ${theme.spacing.unit}px; padding: 0 ${theme.spacing.unit}px;
border: 1px solid ${transparentize(0.9, theme.colors.primary.main)}; border: 1px solid ${transparentize(0.9, theme.colors.primary.main)};
font-family: ${theme.typography.code.fontFamily};
}`}; }`};
& + & { & + & {
margin-left: 0; margin-left: 0;

View File

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

View File

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

View File

@ -39,7 +39,12 @@ export class ApiInfo extends React.Component<ApiInfoProps> {
const license = const license =
(info.license && ( (info.license && (
<InfoSpan> <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> </InfoSpan>
)) || )) ||
null; null;

View File

@ -48,10 +48,10 @@ const CallbackTitleWrapper = styled.button`
`; `;
const CallbackName = styled.span<{ deprecated?: boolean }>` 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; margin-right: 8px;
`; `;
const OperationBadgeStyled = styled(OperationBadge)` 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};`) || ''} (props.expanded && !props.inverted && `border-color: ${props.theme.colors.border.dark};`) || ''}
.${ServerRelativeURL} { .${ServerRelativeURL} {
color: ${props => (props.inverted ? props.theme.colors.text.primary : '#ffffff')} color: ${props => (props.inverted ? props.theme.colors.text.primary : '#ffffff')};
} }
&:focus { &:focus {
box-shadow: inset 0 2px 2px rgba(0, 0, 0, 0.45), 0 2px 0 rgba(128, 128, 128, 0.25); box-shadow: inset 0 2px 2px rgba(0, 0, 0, 0.45), 0 2px 0 rgba(128, 128, 128, 0.25);

View File

@ -0,0 +1,25 @@
import * as React from 'react';
import { TypeFormat, TypePrefix } from '../../common-elements/fields';
import { ConstraintsView } from './FieldContstraints';
import { Pattern } from './Pattern';
import { SchemaModel } from '../../services';
import styled from '../../styled-components';
export function ArrayItemDetails({ schema }: { schema: SchemaModel }) {
if (!schema || (schema.type === 'string' && !schema.constraints.length)) return null;
return (
<Wrapper>
[ items
{schema.displayFormat && <TypeFormat>{` &lt;${schema.displayFormat}&gt; `}</TypeFormat>}
<ConstraintsView constraints={schema.constraints} />
<Pattern schema={schema} />
{schema.items && <ArrayItemDetails schema={schema.items} />} ]
</Wrapper>
);
}
const Wrapper = styled(TypePrefix)`
margin: 0 5px;
vertical-align: text-top;
`;

View File

@ -8,7 +8,7 @@ import { RedocRawOptions } from '../../services/RedocNormalizedOptions';
export interface EnumValuesProps { export interface EnumValuesProps {
values: string[]; values: string[];
type: string | string[]; isArrayType: boolean;
} }
export interface EnumValuesState { export interface EnumValuesState {
@ -27,7 +27,7 @@ export class EnumValues extends React.PureComponent<EnumValuesProps, EnumValuesS
} }
render() { render() {
const { values, type } = this.props; const { values, isArrayType } = this.props;
const { collapsed } = this.state; const { collapsed } = this.state;
// TODO: provide context interface in more elegant way // TODO: provide context interface in more elegant way
@ -55,11 +55,11 @@ export class EnumValues extends React.PureComponent<EnumValuesProps, EnumValuesS
return ( return (
<div> <div>
<FieldLabel> <FieldLabel>
{type === 'array' ? l('enumArray') : ''}{' '} {isArrayType ? l('enumArray') : ''}{' '}
{values.length === 1 ? l('enumSingleValue') : l('enum')}: {values.length === 1 ? l('enumSingleValue') : l('enum')}:
</FieldLabel>{' '} </FieldLabel>{' '}
{displayedItems.map((value, idx) => { {displayedItems.map((value, idx) => {
const exampleValue = enumSkipQuotes ? value : JSON.stringify(value); const exampleValue = enumSkipQuotes ? String(value) : JSON.stringify(value);
return ( return (
<React.Fragment key={idx}> <React.Fragment key={idx}>
<ExampleValue>{exampleValue}</ExampleValue>{' '} <ExampleValue>{exampleValue}</ExampleValue>{' '}

View File

@ -0,0 +1,36 @@
import * as React from 'react';
import { FieldLabel, ExampleValue } from '../../common-elements/fields';
import { getSerializedValue } from '../../utils';
import { l } from '../../services/Labels';
import { FieldModel } from '../../services';
import styled from '../../styled-components';
export function Examples({ field }: { field: FieldModel }) {
if (!field.examples) {
return null;
}
return (
<>
<FieldLabel> {l('examples')}: </FieldLabel>
<ExamplesList>
{Object.values(field.examples).map((example, idx) => {
return (
<li key={idx}>
<ExampleValue>{getSerializedValue(field, example.value)}</ExampleValue> -{' '}
{example.summary || example.description}
</li>
);
})}
</ExamplesList>
</>
);
}
const ExamplesList = styled.ul`
margin-top: 1em;
padding-left: 0;
list-style-position: inside;
`;

View File

@ -32,7 +32,7 @@ export interface FieldProps extends SchemaOptions {
export class Field extends React.Component<FieldProps> { export class Field extends React.Component<FieldProps> {
toggle = () => { toggle = () => {
if (this.props.field.expanded === undefined && this.props.expandByDefault) { if (this.props.field.expanded === undefined && this.props.expandByDefault) {
this.props.field.expanded = false; this.props.field.collapse();
} else { } else {
this.props.field.toggle(); this.props.field.toggle();
} }
@ -94,6 +94,7 @@ export class Field extends React.Component<FieldProps> {
skipReadOnly={this.props.skipReadOnly} skipReadOnly={this.props.skipReadOnly}
skipWriteOnly={this.props.skipWriteOnly} skipWriteOnly={this.props.skipWriteOnly}
showTitle={this.props.showTitle} showTitle={this.props.showTitle}
level={this.props.level}
/> />
</InnerPropertiesWrap> </InnerPropertiesWrap>
</PropertyCellWithInner> </PropertyCellWithInner>

View File

@ -7,18 +7,18 @@ export interface FieldDetailProps {
raw?: boolean; raw?: boolean;
} }
export class FieldDetail extends React.PureComponent<FieldDetailProps> { function FieldDetailComponent({ value, label, raw }: FieldDetailProps) {
render() { if (value === undefined) {
if (this.props.value === undefined) {
return null; return null;
} }
const value = this.props.raw ? this.props.value : JSON.stringify(this.props.value); const stringifyValue = raw ? String(value) : JSON.stringify(value);
return ( return (
<div> <div>
<FieldLabel> {this.props.label} </FieldLabel> <ExampleValue>{value}</ExampleValue> <FieldLabel> {label} </FieldLabel> <ExampleValue>{stringifyValue}</ExampleValue>
</div> </div>
); );
} }
}
export const FieldDetail = React.memo<FieldDetailProps>(FieldDetailComponent);

View File

@ -1,22 +1,19 @@
import * as React from 'react'; import * as React from 'react';
import { import {
PatternLabel,
RecursiveLabel, RecursiveLabel,
TypeFormat, TypeFormat,
TypeName, TypeName,
TypePrefix, TypePrefix,
TypeTitle, TypeTitle,
ToggleButton,
FieldLabel,
ExampleValue,
} from '../../common-elements/fields'; } from '../../common-elements/fields';
import { serializeParameterValue } from '../../utils/openapi'; import { getSerializedValue } from '../../utils';
import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation'; import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation';
import { Markdown } from '../Markdown/Markdown'; import { Markdown } from '../Markdown/Markdown';
import { EnumValues } from './EnumValues'; import { EnumValues } from './EnumValues';
import { Extensions } from './Extensions'; import { Extensions } from './Extensions';
import { FieldProps } from './Field'; import { FieldProps } from './Field';
import { Examples } from './Examples';
import { ConstraintsView } from './FieldContstraints'; import { ConstraintsView } from './FieldContstraints';
import { FieldDetail } from './FieldDetail'; import { FieldDetail } from './FieldDetail';
@ -24,45 +21,36 @@ import { Badge } from '../../common-elements/';
import { l } from '../../services/Labels'; import { l } from '../../services/Labels';
import { OptionsContext } from '../OptionsProvider'; import { OptionsContext } from '../OptionsProvider';
import { FieldModel } from '../../services/models/Field'; import { Pattern } from './Pattern';
import styled from '../../styled-components'; import { ArrayItemDetails } from './ArrayItemDetails';
const MAX_PATTERN_LENGTH = 45; function FieldDetailsComponent(props: FieldProps) {
const { enumSkipQuotes, hideSchemaTitles } = React.useContext(OptionsContext);
export class FieldDetails extends React.PureComponent<FieldProps, { patternShown: boolean }> { const { showExamples, field, renderDiscriminatorSwitch } = props;
state = { const { schema, description, deprecated, extensions, in: _in, const: _const } = field;
patternShown: false, const isArrayType = schema.type === 'array';
};
static contextType = OptionsContext; const rawDefault = enumSkipQuotes || _in === 'header'; // having quotes around header field default values is confusing and inappropriate
togglePattern = () => { const renderedExamples = React.useMemo<JSX.Element | null>(() => {
this.setState({ if (showExamples && (field.example !== undefined || field.examples !== undefined)) {
patternShown: !this.state.patternShown, if (field.examples !== undefined) {
}); return <Examples field={field} />;
};
render() {
const { showExamples, field, renderDiscriminatorSwitch } = this.props;
const { patternShown } = this.state;
const { enumSkipQuotes, hideSchemaTitles, hideSchemaPattern } = this.context;
const { schema, description, example, deprecated, examples } = field;
const rawDefault = !!enumSkipQuotes || field.in === 'header'; // having quotes around header field default values is confusing and inappropriate
let renderedExamples: JSX.Element | null = null;
if (showExamples && (example !== undefined || examples !== undefined)) {
if (examples !== undefined) {
renderedExamples = <Examples field={field} />;
} else { } else {
const label = l('example') + ':'; return (
const raw = !!field.in; <FieldDetail
renderedExamples = <FieldDetail label={label} value={getSerializedValue(field, field.example)} raw={raw} />; label={l('example') + ':'}
value={getSerializedValue(field, field.example)}
raw={Boolean(field.in)}
/>
);
} }
} }
return null;
}, [field, showExamples]);
return ( return (
<div> <div>
<div> <div>
@ -78,35 +66,25 @@ export class FieldDetails extends React.PureComponent<FieldProps, { patternShown
)} )}
{schema.contentEncoding && ( {schema.contentEncoding && (
<TypeFormat> <TypeFormat>
{' '}&lt; {' '}
&lt;
{schema.contentEncoding} {schema.contentEncoding}
&gt;{' '} &gt;{' '}
</TypeFormat> </TypeFormat>
)} )}
{schema.contentMediaType && ( {schema.contentMediaType && (
<TypeFormat> <TypeFormat>
{' '}&lt; {' '}
&lt;
{schema.contentMediaType} {schema.contentMediaType}
&gt;{' '} &gt;{' '}
</TypeFormat> </TypeFormat>
)} )}
{schema.title && !hideSchemaTitles && <TypeTitle> ({schema.title}) </TypeTitle>} {schema.title && !hideSchemaTitles && <TypeTitle> ({schema.title}) </TypeTitle>}
<ConstraintsView constraints={schema.constraints} /> <ConstraintsView constraints={schema.constraints} />
{schema.pattern && !hideSchemaPattern && ( <Pattern schema={schema} />
<>
<PatternLabel>
{patternShown || schema.pattern.length < MAX_PATTERN_LENGTH
? schema.pattern
: `${schema.pattern.substr(0, MAX_PATTERN_LENGTH)}...`}
</PatternLabel>
{schema.pattern.length > MAX_PATTERN_LENGTH && (
<ToggleButton onClick={this.togglePattern}>
{patternShown ? 'Hide pattern' : 'Show pattern'}
</ToggleButton>
)}
</>
)}
{schema.isCircular && <RecursiveLabel> {l('recursive')} </RecursiveLabel>} {schema.isCircular && <RecursiveLabel> {l('recursive')} </RecursiveLabel>}
{isArrayType && schema.items && <ArrayItemDetails schema={schema.items} />}
</div> </div>
{deprecated && ( {deprecated && (
<div> <div>
@ -114,55 +92,21 @@ export class FieldDetails extends React.PureComponent<FieldProps, { patternShown
</div> </div>
)} )}
<FieldDetail raw={rawDefault} label={l('default') + ':'} value={schema.default} /> <FieldDetail raw={rawDefault} label={l('default') + ':'} value={schema.default} />
{!renderDiscriminatorSwitch && <EnumValues type={schema.type} values={schema.enum} />}{' '} {!renderDiscriminatorSwitch && (
<EnumValues isArrayType={isArrayType} values={schema.enum} />
)}{' '}
{renderedExamples} {renderedExamples}
{<Extensions extensions={{ ...field.extensions, ...schema.extensions }} />} <Extensions extensions={{ ...extensions, ...schema.extensions }} />
<div> <div>
<Markdown compact={true} source={description} /> <Markdown compact={true} source={description} />
</div> </div>
{schema.externalDocs && ( {schema.externalDocs && (
<ExternalDocumentation externalDocs={schema.externalDocs} compact={true} /> <ExternalDocumentation externalDocs={schema.externalDocs} compact={true} />
)} )}
{(renderDiscriminatorSwitch && renderDiscriminatorSwitch(this.props)) || null} {(renderDiscriminatorSwitch && renderDiscriminatorSwitch(props)) || null}
{field.const && (<FieldDetail label={l('const') + ':'} value={field.const} />) || null} {(_const && <FieldDetail label={l('const') + ':'} value={_const} />) || null}
</div> </div>
); );
} }
}
function Examples({ field }: { field: FieldModel }) { export const FieldDetails = React.memo<FieldProps>(FieldDetailsComponent);
if (!field.examples) {
return null;
}
return (
<>
<FieldLabel> {l('examples')}: </FieldLabel>
<ExamplesList>
{Object.values(field.examples).map((example, idx) => {
return (
<li key={idx}>
<ExampleValue>{getSerializedValue(field, example.value)}</ExampleValue> - {example.summary || example.description}
</li>
);
})}
</ExamplesList>
</>
);
}
function getSerializedValue(field: FieldModel, example: any) {
if (field.in) {
// decode for better readability in examples: see https://github.com/Redocly/redoc/issues/1138
return decodeURIComponent(serializeParameterValue(field, example));
} else {
return example;
}
}
const ExamplesList = styled.ul`
margin-top: 1em;
padding-left: 0;
list-style-position: inside;
`;

View File

@ -0,0 +1,33 @@
import * as React from 'react';
import { PatternLabel, ToggleButton } from '../../common-elements/fields';
import { OptionsContext } from '../OptionsProvider';
import { SchemaModel } from '../../services';
const MAX_PATTERN_LENGTH = 45;
export function Pattern(props: { schema: SchemaModel }) {
const pattern = props.schema.pattern;
const { hideSchemaPattern } = React.useContext(OptionsContext);
const [isPatternShown, setIsPatternShown] = React.useState(false);
const togglePattern = React.useCallback(
() => setIsPatternShown(!isPatternShown),
[isPatternShown],
);
if (!pattern || hideSchemaPattern) return null;
return (
<>
<PatternLabel>
{isPatternShown || pattern.length < MAX_PATTERN_LENGTH
? pattern
: `${pattern.substr(0, MAX_PATTERN_LENGTH)}...`}
</PatternLabel>
{pattern.length > MAX_PATTERN_LENGTH && (
<ToggleButton onClick={togglePattern}>
{isPatternShown ? 'Hide pattern' : 'Show pattern'}
</ToggleButton>
)}
</>
);
}

View File

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

View File

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

View File

@ -26,7 +26,6 @@ export const StyledMarkdownBlock = styled(
{ compact?: boolean; inline?: boolean } { compact?: boolean; inline?: boolean }
>, >,
)` )`
font-family: ${props => props.theme.typography.fontFamily}; font-family: ${props => props.theme.typography.fontFamily};
font-weight: ${props => props.theme.typography.fontWeightRegular}; font-weight: ${props => props.theme.typography.fontWeightRegular};
line-height: ${props => props.theme.typography.lineHeight}; line-height: ${props => props.theme.typography.lineHeight};
@ -121,7 +120,8 @@ export const StyledMarkdownBlock = styled(
margin: 0; margin: 0;
margin-bottom: 1em; margin-bottom: 1em;
ul, ol { ul,
ol {
margin-bottom: 0; margin-bottom: 0;
margin-top: 0; margin-top: 0;
} }

View File

@ -18,12 +18,6 @@ import { ResponsesList } from '../Responses/ResponsesList';
import { ResponseSamples } from '../ResponseSamples/ResponseSamples'; import { ResponseSamples } from '../ResponseSamples/ResponseSamples';
import { SecurityRequirements } from '../SecurityRequirement/SecurityRequirement'; import { SecurityRequirements } from '../SecurityRequirement/SecurityRequirement';
const OperationRow = styled(Row)`
backface-visibility: hidden;
contain: content;
overflow: hidden;
`;
const Description = styled.div` const Description = styled.div`
margin-bottom: ${({ theme }) => theme.spacing.unit * 6}px; margin-bottom: ${({ theme }) => theme.spacing.unit * 6}px;
`; `;
@ -42,8 +36,8 @@ export class Operation extends React.Component<OperationProps> {
return ( return (
<OptionsContext.Consumer> <OptionsContext.Consumer>
{(options) => ( {options => (
<OperationRow> <Row>
<MiddlePanel> <MiddlePanel>
<H2> <H2>
<ShareLink to={operation.id} /> <ShareLink to={operation.id} />
@ -71,7 +65,7 @@ export class Operation extends React.Component<OperationProps> {
<ResponseSamples operation={operation} /> <ResponseSamples operation={operation} />
<CallbackSamples callbacks={operation.callbacks} /> <CallbackSamples callbacks={operation.callbacks} />
</DarkRightPanel> </DarkRightPanel>
</OperationRow> </Row>
)} )}
</OptionsContext.Consumer> </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 { content, description } = props;
const { isRequestType } = content; const { isRequestType } = content;
return ( return (
@ -76,7 +79,12 @@ export function BodyContent(props: { content: MediaContentModel; description?: s
return ( return (
<> <>
{description !== undefined && <Markdown source={description} />} {description !== undefined && <Markdown source={description} />}
<Schema skipReadOnly={isRequestType} key="schema" schema={schema} /> <Schema
skipReadOnly={isRequestType}
skipWriteOnly={!isRequestType}
key="schema"
schema={schema}
/>
</> </>
); );
}} }}

View File

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

View File

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

View File

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

View File

@ -14,9 +14,15 @@ export interface ResponseTitleProps {
onClick?: () => void; onClick?: () => void;
} }
export class ResponseTitle extends React.PureComponent<ResponseTitleProps> { function ResponseTitleComponent({
render() { title,
const { title, type, empty, code, opened, className, onClick } = this.props; type,
empty,
code,
opened,
className,
onClick,
}: ResponseTitleProps): React.ReactElement {
return ( return (
<button <button
className={className} className={className}
@ -37,4 +43,5 @@ export class ResponseTitle extends React.PureComponent<ResponseTitleProps> {
</button> </button>
); );
} }
}
export const ResponseTitle = React.memo<ResponseTitleProps>(ResponseTitleComponent);

View File

@ -11,16 +11,14 @@ export const StyledResponseTitle = styled(ResponseTitle)`
border-radius: 2px; border-radius: 2px;
margin-bottom: 4px; margin-bottom: 4px;
line-height: 1.5em; line-height: 1.5em;
background-color: #f2f2f2;
cursor: pointer; cursor: pointer;
color: ${(props) => props.theme.colors.responses[props.type].color}; color: ${props => props.theme.colors.responses[props.type].color};
background-color: ${(props) => props.theme.colors.responses[props.type].backgroundColor}; background-color: ${props => props.theme.colors.responses[props.type].backgroundColor};
&:focus { &:focus {
outline: auto; outline: auto ${props => props.theme.colors.responses[props.type].color};
outline-color: ${(props) => props.theme.colors.responses[props.type].color};
} }
${(props) => ${props =>
(props.empty && (props.empty &&
` `
cursor: default; cursor: default;

View File

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

View File

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

View File

@ -16,6 +16,7 @@ export interface SchemaOptions {
showTitle?: boolean; showTitle?: boolean;
skipReadOnly?: boolean; skipReadOnly?: boolean;
skipWriteOnly?: boolean; skipWriteOnly?: boolean;
level?: number;
} }
export interface SchemaProps extends SchemaOptions { export interface SchemaProps extends SchemaOptions {
@ -25,7 +26,9 @@ export interface SchemaProps extends SchemaOptions {
@observer @observer
export class Schema extends React.Component<Partial<SchemaProps>> { export class Schema extends React.Component<Partial<SchemaProps>> {
render() { render() {
const { schema } = this.props; const { schema, ...rest } = this.props;
const level = (rest.level || 0) + 1;
if (!schema) { if (!schema) {
return <em> Schema not provided </em>; return <em> Schema not provided </em>;
} }
@ -50,7 +53,9 @@ export class Schema extends React.Component<Partial<SchemaProps>> {
} }
return ( return (
<ObjectSchema <ObjectSchema
{...{ ...this.props, schema: oneOf![schema.activeOneOf] }} {...rest}
level={level}
schema={oneOf![schema.activeOneOf]}
discriminator={{ discriminator={{
fieldName: discriminatorProp, fieldName: discriminatorProp,
parentSchema: schema, parentSchema: schema,
@ -60,20 +65,20 @@ export class Schema extends React.Component<Partial<SchemaProps>> {
} }
if (oneOf !== undefined) { if (oneOf !== undefined) {
return <OneOfSchema schema={schema} {...this.props} />; return <OneOfSchema schema={schema} {...rest} />;
} }
const types = Array.isArray(type) ? type : [type]; const types = Array.isArray(type) ? type : [type];
if (types.includes('object')) { if (types.includes('object')) {
if (schema.fields?.length) { if (schema.fields?.length) {
return <ObjectSchema {...(this.props as any)} />; return <ObjectSchema {...(this.props as any)} level={level} />;
} }
} else if (types.includes('array')) { } else if (types.includes('array')) {
return <ArraySchema {...(this.props as any)} />; return <ArraySchema {...(this.props as any)} level={level} />;
} }
// TODO: maybe adjust FieldDetails to accept schema // TODO: maybe adjust FieldDetails to accept schema
const field = ({ const field = {
schema, schema,
name: '', name: '',
required: false, required: false,
@ -82,7 +87,7 @@ export class Schema extends React.Component<Partial<SchemaProps>> {
deprecated: false, deprecated: false,
toggle: () => null, toggle: () => null,
expanded: false, 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 ( return (
<div> <div>

View File

@ -100,7 +100,7 @@ export class SearchBox extends React.PureComponent<SearchBoxProps, SearchBoxStat
setResults(results: SearchResult[], term: string) { setResults(results: SearchResult[], term: string) {
this.setState({ this.setState({
results, results,
noResults: results.length === 0 noResults: results.length === 0,
}); });
this.props.marker.mark(term); this.props.marker.mark(term);
} }

View File

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

View File

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

View File

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

View File

@ -44,7 +44,7 @@ export function StoreBuilder(props: StoreBuilderProps) {
setResolvedSpec(resolved); setResolvedSpec(resolved);
} }
load(); load();
}, [spec, specUrl]) }, [spec, specUrl]);
const store = React.useMemo(() => { const store = React.useMemo(() => {
if (!resolvedSpec) return null; if (!resolvedSpec) return null;
@ -56,13 +56,14 @@ export function StoreBuilder(props: StoreBuilderProps) {
} }
throw e; throw e;
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [resolvedSpec, specUrl, options]); }, [resolvedSpec, specUrl, options]);
React.useEffect(() => { React.useEffect(() => {
if (store && onLoaded) { if (store && onLoaded) {
onLoaded(); onLoaded();
} }
}, [store, onLoaded]) }, [store, onLoaded]);
return children({ return children({
loading: !store, loading: !store,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,6 +5,11 @@ import { isNumeric, mergeObjects } from '../utils/helpers';
import { LabelsConfigRaw, setRedocLabels } from './Labels'; import { LabelsConfigRaw, setRedocLabels } from './Labels';
import { MDXComponentMeta } from './MarkdownRenderer'; import { MDXComponentMeta } from './MarkdownRenderer';
export enum SideNavStyleEnum {
SummaryOnly = 'summary-only',
PathOnly = 'path-only',
}
export interface RedocRawOptions { export interface RedocRawOptions {
theme?: ThemeInterface; theme?: ThemeInterface;
scrollYOffset?: number | string | (() => number); scrollYOffset?: number | string | (() => number);
@ -13,6 +18,8 @@ export interface RedocRawOptions {
requiredPropsFirst?: boolean | string; requiredPropsFirst?: boolean | string;
sortPropsAlphabetically?: boolean | string; sortPropsAlphabetically?: boolean | string;
sortEnumValuesAlphabetically?: boolean | string; sortEnumValuesAlphabetically?: boolean | string;
sortOperationsAlphabetically?: boolean | string;
sortTagsAlphabetically?: boolean | string;
noAutoAuth?: boolean | string; noAutoAuth?: boolean | string;
nativeScrollbars?: boolean | string; nativeScrollbars?: boolean | string;
pathInMiddlePanel?: boolean | string; pathInMiddlePanel?: boolean | string;
@ -22,6 +29,7 @@ export interface RedocRawOptions {
disableSearch?: boolean | string; disableSearch?: boolean | string;
onlyRequiredInSamples?: boolean | string; onlyRequiredInSamples?: boolean | string;
showExtensions?: boolean | string | string[]; showExtensions?: boolean | string | string[];
sideNavStyle?: SideNavStyleEnum;
hideSingleRequestSampleTab?: boolean | string; hideSingleRequestSampleTab?: boolean | string;
menuToggle?: boolean | string; menuToggle?: boolean | string;
jsonSampleExpandLevel?: number | string | 'all'; jsonSampleExpandLevel?: number | string | 'all';
@ -29,6 +37,8 @@ export interface RedocRawOptions {
simpleOneOfTypeLabel?: boolean | string; simpleOneOfTypeLabel?: boolean | string;
payloadSampleIdx?: number; payloadSampleIdx?: number;
expandSingleSchemaField?: boolean | string; expandSingleSchemaField?: boolean | string;
schemaExpansionLevel?: number | string | 'all';
showObjectSchemaExamples?: boolean | string;
unstable_ignoreMimeParameters?: boolean; unstable_ignoreMimeParameters?: boolean;
@ -65,6 +75,12 @@ function argValueToNumber(value: number | string | undefined): number | undefine
} }
} }
function argValueToExpandLevel(value?: number | string | undefined, defaultValue = 0): number {
if (value === 'all') return Infinity;
return argValueToNumber(value) || defaultValue;
}
export class RedocNormalizedOptions { export class RedocNormalizedOptions {
static normalizeExpandResponses(value: RedocRawOptions['expandResponses']) { static normalizeExpandResponses(value: RedocRawOptions['expandResponses']) {
if (value === 'all') { if (value === 'all') {
@ -72,7 +88,7 @@ export class RedocNormalizedOptions {
} }
if (typeof value === 'string') { if (typeof value === 'string') {
const res = {}; const res = {};
value.split(',').forEach((code) => { value.split(',').forEach(code => {
res[code.trim()] = true; res[code.trim()] = true;
}); });
return res; return res;
@ -138,7 +154,23 @@ export class RedocNormalizedOptions {
case 'false': case 'false':
return false; return false;
default: 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 +213,8 @@ export class RedocNormalizedOptions {
requiredPropsFirst: boolean; requiredPropsFirst: boolean;
sortPropsAlphabetically: boolean; sortPropsAlphabetically: boolean;
sortEnumValuesAlphabetically: boolean; sortEnumValuesAlphabetically: boolean;
sortOperationsAlphabetically: boolean;
sortTagsAlphabetically: boolean;
noAutoAuth: boolean; noAutoAuth: boolean;
nativeScrollbars: boolean; nativeScrollbars: boolean;
pathInMiddlePanel: boolean; pathInMiddlePanel: boolean;
@ -189,6 +223,7 @@ export class RedocNormalizedOptions {
disableSearch: boolean; disableSearch: boolean;
onlyRequiredInSamples: boolean; onlyRequiredInSamples: boolean;
showExtensions: boolean | string[]; showExtensions: boolean | string[];
sideNavStyle: SideNavStyleEnum;
hideSingleRequestSampleTab: boolean; hideSingleRequestSampleTab: boolean;
menuToggle: boolean; menuToggle: boolean;
jsonSampleExpandLevel: number; jsonSampleExpandLevel: number;
@ -197,6 +232,8 @@ export class RedocNormalizedOptions {
simpleOneOfTypeLabel: boolean; simpleOneOfTypeLabel: boolean;
payloadSampleIdx: number; payloadSampleIdx: number;
expandSingleSchemaField: boolean; expandSingleSchemaField: boolean;
schemaExpansionLevel: number;
showObjectSchemaExamples: boolean;
/* tslint:disable-next-line */ /* tslint:disable-next-line */
unstable_ignoreMimeParameters: boolean; unstable_ignoreMimeParameters: boolean;
@ -239,6 +276,8 @@ export class RedocNormalizedOptions {
this.requiredPropsFirst = argValueToBoolean(raw.requiredPropsFirst); this.requiredPropsFirst = argValueToBoolean(raw.requiredPropsFirst);
this.sortPropsAlphabetically = argValueToBoolean(raw.sortPropsAlphabetically); this.sortPropsAlphabetically = argValueToBoolean(raw.sortPropsAlphabetically);
this.sortEnumValuesAlphabetically = argValueToBoolean(raw.sortEnumValuesAlphabetically); this.sortEnumValuesAlphabetically = argValueToBoolean(raw.sortEnumValuesAlphabetically);
this.sortOperationsAlphabetically = argValueToBoolean(raw.sortOperationsAlphabetically);
this.sortTagsAlphabetically = argValueToBoolean(raw.sortTagsAlphabetically);
this.noAutoAuth = argValueToBoolean(raw.noAutoAuth); this.noAutoAuth = argValueToBoolean(raw.noAutoAuth);
this.nativeScrollbars = argValueToBoolean(raw.nativeScrollbars); this.nativeScrollbars = argValueToBoolean(raw.nativeScrollbars);
this.pathInMiddlePanel = argValueToBoolean(raw.pathInMiddlePanel); this.pathInMiddlePanel = argValueToBoolean(raw.pathInMiddlePanel);
@ -247,6 +286,7 @@ export class RedocNormalizedOptions {
this.disableSearch = argValueToBoolean(raw.disableSearch); this.disableSearch = argValueToBoolean(raw.disableSearch);
this.onlyRequiredInSamples = argValueToBoolean(raw.onlyRequiredInSamples); this.onlyRequiredInSamples = argValueToBoolean(raw.onlyRequiredInSamples);
this.showExtensions = RedocNormalizedOptions.normalizeShowExtensions(raw.showExtensions); this.showExtensions = RedocNormalizedOptions.normalizeShowExtensions(raw.showExtensions);
this.sideNavStyle = RedocNormalizedOptions.normalizeSideNavStyle(raw.sideNavStyle);
this.hideSingleRequestSampleTab = argValueToBoolean(raw.hideSingleRequestSampleTab); this.hideSingleRequestSampleTab = argValueToBoolean(raw.hideSingleRequestSampleTab);
this.menuToggle = argValueToBoolean(raw.menuToggle, true); this.menuToggle = argValueToBoolean(raw.menuToggle, true);
this.jsonSampleExpandLevel = RedocNormalizedOptions.normalizeJsonSampleExpandLevel( this.jsonSampleExpandLevel = RedocNormalizedOptions.normalizeJsonSampleExpandLevel(
@ -257,6 +297,8 @@ export class RedocNormalizedOptions {
this.simpleOneOfTypeLabel = argValueToBoolean(raw.simpleOneOfTypeLabel); this.simpleOneOfTypeLabel = argValueToBoolean(raw.simpleOneOfTypeLabel);
this.payloadSampleIdx = RedocNormalizedOptions.normalizePayloadSampleIdx(raw.payloadSampleIdx); this.payloadSampleIdx = RedocNormalizedOptions.normalizePayloadSampleIdx(raw.payloadSampleIdx);
this.expandSingleSchemaField = argValueToBoolean(raw.expandSingleSchemaField); this.expandSingleSchemaField = argValueToBoolean(raw.expandSingleSchemaField);
this.schemaExpansionLevel = argValueToExpandLevel(raw.schemaExpansionLevel);
this.showObjectSchemaExamples = argValueToBoolean(raw.showObjectSchemaExamples);
this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters); this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters);
@ -266,7 +308,7 @@ export class RedocNormalizedOptions {
this.maxDisplayedEnumValues = argValueToNumber(raw.maxDisplayedEnumValues); this.maxDisplayedEnumValues = argValueToNumber(raw.maxDisplayedEnumValues);
const ignoreNamedSchemas = Array.isArray(raw.ignoreNamedSchemas) const ignoreNamedSchemas = Array.isArray(raw.ignoreNamedSchemas)
? raw.ignoreNamedSchemas ? raw.ignoreNamedSchemas
: raw.ignoreNamedSchemas?.split(',').map((s) => s.trim()); : raw.ignoreNamedSchemas?.split(',').map(s => s.trim());
this.ignoreNamedSchemas = new Set(ignoreNamedSchemas); this.ignoreNamedSchemas = new Set(ignoreNamedSchemas);
this.hideSchemaPattern = argValueToBoolean(raw.hideSchemaPattern); this.hideSchemaPattern = argValueToBoolean(raw.hideSchemaPattern);
this.generatedPayloadSamplesMaxDepth = this.generatedPayloadSamplesMaxDepth =

View File

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

View File

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

View File

@ -28,7 +28,10 @@ export class SpecStore {
this.externalDocs = this.parser.spec.externalDocs; this.externalDocs = this.parser.spec.externalDocs;
this.contentItems = MenuBuilder.buildStructure(this.parser, this.options); this.contentItems = MenuBuilder.buildStructure(this.parser, this.options);
this.securitySchemes = new SecuritySchemesModel(this.parser); this.securitySchemes = new SecuritySchemesModel(this.parser);
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); this.webhooks = new WebhookModel(this.parser, options, webhookPath);
} }
} }

View File

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

View File

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

View File

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

View File

@ -39,5 +39,19 @@ describe('Models', () => {
const resp = new ResponseModel({ ...props, code: 'default', defaultAsError: true }); const resp = new ResponseModel({ ...props, code: 'default', defaultAsError: true });
expect(resp.type).toEqual('error'); 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

@ -41,7 +41,7 @@ const DEFAULT_SERIALIZATION: Record<
*/ */
export class FieldModel { export class FieldModel {
@observable @observable
expanded: boolean | undefined = false; expanded: boolean | undefined = undefined;
schema: SchemaModel; schema: SchemaModel;
name: string; name: string;
@ -120,4 +120,14 @@ export class FieldModel {
toggle() { toggle() {
this.expanded = !this.expanded; this.expanded = !this.expanded;
} }
@action
collapse(): void {
this.expanded = false;
}
@action
expand(): void {
this.expanded = true;
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,9 @@ describe('#loadAndBundleSpec', () => {
}); });
it('should load And Bundle Spec demo/openapi-3-1.yaml', async () => { 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); const bundledSpec = await loadAndBundleSpec(spec);
expect(bundledSpec).toMatchSnapshot(); expect(bundledSpec).toMatchSnapshot();
}); });

View File

@ -0,0 +1,50 @@
import { objectHas, objectSet } from '../object';
describe('object utils', () => {
let obj;
beforeEach(() => {
obj = {
a: {
b: {
c: {
d: 'd',
},
c1: 'c1',
},
b1: 'b1',
},
a1: 'a1',
};
});
describe('objectHas function', () => {
it('should check if the obj has path as string', () => {
expect(objectHas(obj, 'a.b.c')).toBeTruthy();
expect(objectHas(obj, 'a.b.c1')).toBeTruthy();
expect(objectHas(obj, 'a.b.c.d')).toBeTruthy();
expect(objectHas(obj, 'a.b.c1.d')).toBeFalsy();
});
it('should check if the obj has path as array', () => {
expect(objectHas(obj, ['a', 'b', 'c'])).toBeTruthy();
expect(objectHas(obj, ['a', 'b', 'c1'])).toBeTruthy();
expect(objectHas(obj, ['a', 'b', 'c', 'd'])).toBeTruthy();
expect(objectHas(obj, ['a', 'b', 'c1', 'd'])).toBeFalsy();
});
});
describe('objectSet function', () => {
it('should set value by path as string', () => {
expect(objectHas(obj, 'a.b.c1.d')).toBeFalsy();
objectSet(obj, 'a.b.c1', { d: 'd' });
expect(objectHas(obj, 'a.b.c1.d')).toBeTruthy();
});
it('should set value by path as array', () => {
expect(objectHas(obj, ['a', 'b', 'c1', 'd'])).toBeFalsy();
objectSet(obj, ['a', 'b', 'c1'], { d: 'd' });
expect(objectHas(obj, ['a', 'b', 'c1', 'd'])).toBeTruthy();
});
});
});

View File

@ -10,10 +10,18 @@ import {
pluralizeType, pluralizeType,
serializeParameterValue, serializeParameterValue,
sortByRequired, sortByRequired,
humanizeNumberRange,
getContentWithLegacyExamples,
getDefinitionName,
} from '../'; } from '../';
import { FieldModel, OpenAPIParser, RedocNormalizedOptions } from '../../services'; import { FieldModel, OpenAPIParser, RedocNormalizedOptions } from '../../services';
import { OpenAPIParameter, OpenAPIParameterLocation, OpenAPIParameterStyle } from '../../types'; import {
OpenAPIMediaType,
OpenAPIParameter,
OpenAPIParameterLocation,
OpenAPIParameterStyle,
} from '../../types';
import { expandDefaultServerVariables } from '../openapi'; import { expandDefaultServerVariables } from '../openapi';
describe('Utils', () => { describe('Utils', () => {
@ -103,7 +111,7 @@ describe('Utils', () => {
it('Should return pathName if no summary, operationId, description', () => { it('Should return pathName if no summary, operationId, description', () => {
const operation = { const operation = {
pathName: '/sandbox/test' pathName: '/sandbox/test',
}; };
expect(getOperationSummary(operation as any)).toBe('/sandbox/test'); expect(getOperationSummary(operation as any)).toBe('/sandbox/test');
}); });
@ -174,7 +182,7 @@ describe('Utils', () => {
expect(isPrimitiveType(schema)).toEqual(false); expect(isPrimitiveType(schema)).toEqual(false);
}); });
it('should return true for array contains object and schema hasn\'t properties', () => { it("should return true for array contains object and schema hasn't properties", () => {
const schema = { const schema = {
type: ['object', 'string'], type: ['object', 'string'],
}; };
@ -233,7 +241,7 @@ describe('Utils', () => {
items: { items: {
type: 'array', type: 'array',
items: { items: {
type: 'string' type: 'string',
}, },
}, },
}; };
@ -410,12 +418,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', () => { describe('openapi humanizeConstraints', () => {
const itemConstraintSchema = ( const itemConstraintSchema = (
min?: number, min?: number,
max?: number, max?: number,
multipleOf?: number, multipleOf?: number,
uniqueItems?: boolean uniqueItems?: boolean,
) => ({ type: 'array', minItems: min, maxItems: max, multipleOf, uniqueItems }); ) => ({ type: 'array', minItems: min, maxItems: max, multipleOf, uniqueItems });
it('should not have a humanized constraint without schema constraints', () => { it('should not have a humanized constraint without schema constraints', () => {
@ -455,9 +533,9 @@ describe('Utils', () => {
}); });
it('should have a humanized constraint when uniqueItems is set', () => { it('should have a humanized constraint when uniqueItems is set', () => {
expect(humanizeConstraints(itemConstraintSchema(undefined, undefined, undefined, true))).toContain( expect(
'unique', humanizeConstraints(itemConstraintSchema(undefined, undefined, undefined, true)),
); ).toContain('unique');
}); });
}); });
@ -1090,4 +1168,116 @@ 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']);
});
});
describe('getDefinitionName', () => {
test('should return the name if pointer match regex', () => {
expect(getDefinitionName('#/components/schemas/Call')).toEqual('Call');
});
test("should return the `undefined` if pointer not match regex or it's absent", () => {
expect(getDefinitionName('#/test/path/Call')).toBeUndefined();
expect(getDefinitionName()).toBeUndefined();
});
});
}); });

View File

@ -24,13 +24,16 @@ export function html2Str(html: string): string {
.join(' '); .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) { export function scrollIntoViewIfNeeded(el: HTMLElement, centerIfNeeded = true) {
(Element as any).prototype.scrollIntoViewIfNeeded = function(centerIfNeeded) { const parent = el.parentNode as HTMLElement | null;
centerIfNeeded = arguments.length === 0 ? true : !!centerIfNeeded; if (!parent) {
return;
const parent = this.parentNode; }
const parentComputedStyle = window.getComputedStyle(parent, undefined); const parentComputedStyle = window.getComputedStyle(parent, undefined);
const parentBorderTopWidth = parseInt( const parentBorderTopWidth = parseInt(
parentComputedStyle.getPropertyValue('border-top-width'), parentComputedStyle.getPropertyValue('border-top-width'),
@ -40,36 +43,35 @@ if (typeof Element !== 'undefined' && !(Element as any).prototype.scrollIntoView
parentComputedStyle.getPropertyValue('border-left-width'), parentComputedStyle.getPropertyValue('border-left-width'),
10, 10,
); );
const overTop = this.offsetTop - parent.offsetTop < parent.scrollTop; const overTop = el.offsetTop - parent.offsetTop < parent.scrollTop;
const overBottom = const overBottom =
this.offsetTop - parent.offsetTop + this.clientHeight - parentBorderTopWidth > el.offsetTop - parent.offsetTop + el.clientHeight - parentBorderTopWidth >
parent.scrollTop + parent.clientHeight; parent.scrollTop + parent.clientHeight;
const overLeft = this.offsetLeft - parent.offsetLeft < parent.scrollLeft; const overLeft = el.offsetLeft - parent.offsetLeft < parent.scrollLeft;
const overRight = const overRight =
this.offsetLeft - parent.offsetLeft + this.clientWidth - parentBorderLeftWidth > el.offsetLeft - parent.offsetLeft + el.clientWidth - parentBorderLeftWidth >
parent.scrollLeft + parent.clientWidth; parent.scrollLeft + parent.clientWidth;
const alignWithTop = overTop && !overBottom; const alignWithTop = overTop && !overBottom;
if ((overTop || overBottom) && centerIfNeeded) { if ((overTop || overBottom) && centerIfNeeded) {
parent.scrollTop = parent.scrollTop =
this.offsetTop - el.offsetTop -
parent.offsetTop - parent.offsetTop -
parent.clientHeight / 2 - parent.clientHeight / 2 -
parentBorderTopWidth + parentBorderTopWidth +
this.clientHeight / 2; el.clientHeight / 2;
} }
if ((overLeft || overRight) && centerIfNeeded) { if ((overLeft || overRight) && centerIfNeeded) {
parent.scrollLeft = parent.scrollLeft =
this.offsetLeft - el.offsetLeft -
parent.offsetLeft - parent.offsetLeft -
parent.clientWidth / 2 - parent.clientWidth / 2 -
parentBorderLeftWidth + parentBorderLeftWidth +
this.clientWidth / 2; el.clientWidth / 2;
} }
if ((overTop || overBottom || overLeft || overRight) && !centerIfNeeded) { if ((overTop || overBottom || overLeft || overRight) && !centerIfNeeded) {
this.scrollIntoView(alignWithTop); el.scrollIntoView(alignWithTop);
} }
};
} }

View File

@ -50,7 +50,7 @@ export function flattenByProp<T extends object, P extends keyof T>(
for (const item of items) { for (const item of items) {
res.push(item); res.push(item);
if (item[prop]) { 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 './decorators';
export * from './debug'; export * from './debug';
export * from './memoize'; 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 config = new Config({});
const bundleOpts = { const bundleOpts = {
config, config,
base: IS_BROWSER ? window.location.href : process.cwd() base: IS_BROWSER ? window.location.href : process.cwd(),
} };
if (IS_BROWSER) { if (IS_BROWSER) {
config.resolve.http.customFetch = global.fetch; config.resolve.http.customFetch = global.fetch;
@ -24,13 +24,15 @@ export async function loadAndBundleSpec(specUrlOrObject: object | string): Promi
if (typeof specUrlOrObject === 'object' && specUrlOrObject !== null) { if (typeof specUrlOrObject === 'object' && specUrlOrObject !== null) {
bundleOpts['doc'] = { bundleOpts['doc'] = {
source: { absoluteRef: '' } as Source, source: { absoluteRef: '' } as Source,
parsed: specUrlOrObject parsed: specUrlOrObject,
} as Document } as Document;
} else { } else {
bundleOpts['ref'] = specUrlOrObject; bundleOpts['ref'] = specUrlOrObject;
} }
const { bundle: { parsed } } = await bundle(bundleOpts); const {
bundle: { parsed },
} = await bundle(bundleOpts);
return parsed.swagger !== undefined ? convertSwagger2OpenAPI(parsed) : parsed; 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>) { export function memoize<T>(target: any, name: string, descriptor: TypedPropertyDescriptor<T>) {
if (typeof descriptor.value === 'function') { 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') { } else if (typeof descriptor.get === 'function') {
return _memoizeGetter(target, name, descriptor) as TypedPropertyDescriptor<T>; return _memoizeGetter(target, name, descriptor) as TypedPropertyDescriptor<T>;
} else { } else {

28
src/utils/object.ts Normal file
View File

@ -0,0 +1,28 @@
export function objectHas(object: GenericObject, path: string | Array<string>): boolean {
let _path = <Array<string>>path;
if (typeof path === 'string') {
_path = path.split('.');
}
return _path.every((key: string) => {
if (typeof object != 'object' || object === null || !(key in object)) return false;
object = object[key];
return true;
});
}
export function objectSet(object: GenericObject, path: string | Array<string>, value: any): void {
let _path = <Array<string>>path;
if (typeof path === 'string') {
_path = path.split('.');
}
const limit = _path.length - 1;
for (let i = 0; i < limit; ++i) {
const key = _path[i];
object = object[key] ?? (object[key] = {});
}
const key = _path[limit];
object[key] = value;
}

View File

@ -9,6 +9,8 @@ import {
OpenAPIMediaType, OpenAPIMediaType,
OpenAPIParameter, OpenAPIParameter,
OpenAPIParameterStyle, OpenAPIParameterStyle,
OpenAPIRequestBody,
OpenAPIResponse,
OpenAPISchema, OpenAPISchema,
OpenAPIServer, OpenAPIServer,
Referenced, Referenced,
@ -113,7 +115,10 @@ export function detectType(schema: OpenAPISchema): string {
return 'any'; 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) { if (schema.oneOf !== undefined || schema.anyOf !== undefined) {
return false; return false;
} }
@ -122,7 +127,8 @@ export function isPrimitiveType(schema: OpenAPISchema, type: string | string[] |
const isArray = Array.isArray(type); const isArray = Array.isArray(type);
if (type === 'object' || (isArray && type?.includes('object'))) { if (type === 'object' || (isArray && type?.includes('object'))) {
isPrimitive = schema.properties !== undefined isPrimitive =
schema.properties !== undefined
? Object.keys(schema.properties).length === 0 ? Object.keys(schema.properties).length === 0
: schema.additionalProperties === undefined; : schema.additionalProperties === undefined;
} }
@ -362,6 +368,15 @@ export function serializeParameterValue(
} }
} }
export function getSerializedValue(field: FieldModel, example: any) {
if (field.in) {
// decode for better readability in examples: see https://github.com/Redocly/redoc/issues/1138
return decodeURIComponent(serializeParameterValue(field, example));
} else {
return example;
}
}
export function langFromMime(contentType: string): string { export function langFromMime(contentType: string): string {
if (contentType.search(/xml/i) !== -1) { if (contentType.search(/xml/i) !== -1) {
return 'xml'; return 'xml';
@ -369,14 +384,15 @@ export function langFromMime(contentType: string): string {
return 'clike'; return 'clike';
} }
const DEFINITION_NAME_REGEX = /^#\/components\/(schemas|pathItems)\/([^/]+)$/;
export function isNamedDefinition(pointer?: string): boolean { export function isNamedDefinition(pointer?: string): boolean {
return /^#\/components\/(schemas|pathItems)\/[^\/]+$/.test(pointer || ''); return DEFINITION_NAME_REGEX.test(pointer || '');
} }
export function getDefinitionName(pointer?: string): string | undefined { export function getDefinitionName(pointer?: string): string | undefined {
if (!pointer) return undefined; const [name] = pointer?.match(DEFINITION_NAME_REGEX)?.reverse() || [];
const match = pointer.match(/^#\/components\/(schemas|pathItems)\/([^\/]+)$/); return name;
return match === null ? undefined : match[1]
} }
function humanizeMultipleOfConstraint(multipleOf: number | undefined): string | undefined { function humanizeMultipleOfConstraint(multipleOf: number | undefined): string | undefined {
@ -415,6 +431,29 @@ function humanizeRangeConstraint(
return stringRange; 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[] { export function humanizeConstraints(schema: OpenAPISchema): string[] {
const res: string[] = []; const res: string[] = [];
@ -433,33 +472,7 @@ export function humanizeConstraints(schema: OpenAPISchema): string[] {
res.push(multipleOfConstraint); res.push(multipleOfConstraint);
} }
let numberRange; const numberRange = humanizeNumberRange(schema);
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}]`
}
if (numberRange !== undefined) { if (numberRange !== undefined) {
res.push(numberRange); res.push(numberRange);
} }
@ -637,3 +650,33 @@ export function pluralizeType(displayType: string): string {
.map(type => type.replace(/^(string|object|number|integer|array|boolean)s?( ?.*)/, '$1s$2')) .map(type => type.replace(/^(string|object|number|integer|array|boolean)s?( ?.*)/, '$1s$2'))
.join(' or '); .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

@ -1,6 +1,4 @@
/* tslint:disable:no-implicit-dependencies */ import { objectHas, objectSet } from './object';
import { has, set } from 'lodash';
function traverseComponent(root, fn) { function traverseComponent(root, fn) {
if (!root) { if (!root) {
@ -20,8 +18,8 @@ export function filterPropsDeep<T extends object>(component: T, paths: string[])
traverseComponent(component, comp => { traverseComponent(component, comp => {
if (comp.props) { if (comp.props) {
for (const path of paths) { for (const path of paths) {
if (has(comp.props, path)) { if (objectHas(comp.props, path)) {
set(comp.props, path, '<<<filtered>>>'); objectSet(comp.props, path, '<<<filtered>>>');
} }
} }
} }

View File

@ -13,27 +13,12 @@
"importHelpers": true, "importHelpers": true,
"outDir": "lib", "outDir": "lib",
"pretty": true, "pretty": true,
"lib": [ "lib": ["es2015", "es2016", "es2017", "dom", "WebWorker.ImportScripts"],
"es2015",
"es2016",
"es2017",
"dom",
"WebWorker.ImportScripts"
],
"jsx": "react", "jsx": "react",
"types": [ "types": ["webpack", "webpack-env", "jest"]
"webpack",
"webpack-env",
"jest"
]
}, },
"compileOnSave": false, "compileOnSave": false,
"exclude": [ "exclude": ["node_modules", ".tmp", "lib", "e2e/**"],
"node_modules",
".tmp",
"lib",
"e2e/**"
],
"include": [ "include": [
"cli/index.ts", "cli/index.ts",
"./custom.d.ts", "./custom.d.ts",

View File

@ -2,7 +2,7 @@
import ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); import ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
import * as webpack from 'webpack'; import * as webpack from 'webpack';
import * as path from 'path'; import * as path from 'path';
import { getBabelLoader, webpackIgnore } from './config/webpack-utils'; import { webpackIgnore } from './config/webpack-utils';
const nodeExternals = require('webpack-node-externals')({ const nodeExternals = require('webpack-node-externals')({
// bundle in modules that need transpiling + non-js (e.g. css) // bundle in modules that need transpiling + non-js (e.g. css)
@ -32,10 +32,14 @@ const BANNER = `ReDoc - OpenAPI/Swagger-generated API Reference Documentation
Version: ${VERSION} Version: ${VERSION}
Repo: https://github.com/Redocly/redoc`; Repo: https://github.com/Redocly/redoc`;
export default (env: { standalone?: boolean } = {}) => ({ export default (env: { standalone?: boolean; browser?: boolean } = {}) => ({
entry: env.standalone ? ['./src/polyfills.ts', './src/standalone.tsx'] : './src/index.ts', entry: env.standalone ? ['./src/polyfills.ts', './src/standalone.tsx'] : './src/index.ts',
output: { output: {
filename: env.standalone ? 'redoc.standalone.js' : 'redoc.lib.js', filename: env.standalone
? 'redoc.standalone.js'
: env.browser
? 'redoc.browser.lib.js'
: 'redoc.lib.js',
path: path.join(__dirname, '/bundles'), path: path.join(__dirname, '/bundles'),
library: 'Redoc', library: 'Redoc',
libraryTarget: 'umd', libraryTarget: 'umd',
@ -46,14 +50,15 @@ export default (env: { standalone?: boolean } = {}) => ({
extensions: ['.ts', '.tsx', '.js', '.mjs', '.json'], extensions: ['.ts', '.tsx', '.js', '.mjs', '.json'],
fallback: { fallback: {
path: require.resolve('path-browserify'), path: require.resolve('path-browserify'),
buffer: require.resolve('buffer'),
http: false, http: false,
fs: false, fs: path.resolve(__dirname, 'src/empty.js'),
os: false, os: path.resolve(__dirname, 'src/empty.js'),
} tty: path.resolve(__dirname, 'src/empty.js'),
},
}, },
performance: false, performance: false,
// target: 'node', externalsPresets: env.standalone || env.browser ? {} : { node: true },
externalsPresets: env.standalone ? {} : { node: true },
externals: env.standalone externals: env.standalone
? { ? {
esprima: 'null', esprima: 'null',
@ -74,32 +79,27 @@ export default (env: { standalone?: boolean } = {}) => ({
rules: [ rules: [
{ {
test: /\.(tsx?|[cm]?js)$/, test: /\.(tsx?|[cm]?js)$/,
use: [getBabelLoader({useBuiltIns: !!env.standalone})], loader: 'esbuild-loader',
exclude: { options: {
and: [/node_modules/], loader: 'tsx',
not: { target: 'es2015',
or: [ tsconfigRaw: require('./tsconfig.json'),
/swagger2openapi/,
/reftools/,
/openapi-sampler/,
/mobx/,
/oas-resolver/,
/oas-kit-common/,
/oas-schema-walker/,
/\@redocly\/openapi-core/,
/colorette/,
],
},
}, },
exclude: [/node_modules/],
}, },
{ {
test: /\.css$/, test: /\.css$/,
use: { use: [
loader: 'css-loader', 'isomorphic-style-loader',
'css-loader',
{
loader: 'esbuild-loader',
options: { options: {
sourceMap: false, loader: 'css',
minify: true,
}, },
}, },
],
}, },
], ],
}, },
@ -113,6 +113,9 @@ export default (env: { standalone?: boolean } = {}) => ({
}), }),
new ForkTsCheckerWebpackPlugin({ logger: { infrastructure: 'silent', issues: 'console' } }), new ForkTsCheckerWebpackPlugin({ logger: { infrastructure: 'silent', issues: 'console' } }),
new webpack.BannerPlugin(BANNER), new webpack.BannerPlugin(BANNER),
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
}),
webpackIgnore(/js-yaml\/dumper\.js$/), webpackIgnore(/js-yaml\/dumper\.js$/),
env.standalone ? webpackIgnore(/^\.\/SearchWorker\.worker$/) : undefined, env.standalone ? webpackIgnore(/^\.\/SearchWorker\.worker$/) : undefined,
].filter(Boolean), ].filter(Boolean),