feat: Add bundles

This commit is contained in:
Till Kolter 2020-08-25 23:36:10 +02:00
parent 49f04d321f
commit 422ec09771
34 changed files with 15457 additions and 426 deletions

2
.gitignore vendored
View File

@ -24,7 +24,7 @@ node_modules
lib/ lib/
stats.json stats.json
cypress/ cypress/
bundles/ # bundles/
typings/* typings/*
!typings/styled-patch.d.ts !typings/styled-patch.d.ts
cli/index.js cli/index.js

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

14845
bundles/redoc.lib.js Normal file

File diff suppressed because one or more lines are too long

1
bundles/redoc.lib.js.map Normal file

File diff suppressed because one or more lines are too long

110
bundles/redoc.standalone.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { render } from 'react-dom'; import { render } from 'react-dom';
import styled from 'styled-components'; import styled from '../src/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';

View File

@ -30,14 +30,6 @@ info:
And that allows cross-domain communication from the browser. And that allows cross-domain communication from the browser.
All responses have a wildcard same-origin which makes them completely public and accessible to everyone, including any code on any site. All responses have a wildcard same-origin which makes them completely public and accessible to everyone, including any code on any site.
# Authentication
Petstore offers two forms of authentication:
- API Key
- OAuth2
OAuth2 - an open protocol to allow secure authorization in a simple
and standard method from web, mobile and desktop applications.
<SecurityDefinitions /> <SecurityDefinitions />
version: 1.0.0 version: 1.0.0
@ -1193,7 +1185,7 @@ x-webhooks:
summary: New pet summary: New pet
description: Information about a new pet in the systems description: Information about a new pet in the systems
operationId: newPet operationId: newPet
tags: tags:
- pet - pet
requestBody: requestBody:
content: content:
@ -1202,4 +1194,4 @@ x-webhooks:
$ref: "#/components/schemas/Pet" $ref: "#/components/schemas/Pet"
responses: responses:
"200": "200":
description: Return a 200 status to indicate that the data was received successfully description: Return a 200 status to indicate that the data was received successfully

View File

@ -8,6 +8,7 @@ import { Redoc, RedocProps } from '../../src/components/Redoc/Redoc';
import { AppStore } from '../../src/services/AppStore'; import { AppStore } from '../../src/services/AppStore';
import { RedocRawOptions } from '../../src/services/RedocNormalizedOptions'; import { RedocRawOptions } from '../../src/services/RedocNormalizedOptions';
import { loadAndBundleSpec } from '../../src/utils/loadAndBundleSpec'; import { loadAndBundleSpec } from '../../src/utils/loadAndBundleSpec';
import markdown from './markdown';
const renderRoot = (props: RedocProps) => const renderRoot = (props: RedocProps) =>
render( render(
@ -32,11 +33,12 @@ const options: RedocRawOptions = {
hideShelfIcon: true, hideShelfIcon: true,
maxDisplayedEnumValues: 3, maxDisplayedEnumValues: 3,
nativeScrollbars: false, nativeScrollbars: false,
noAutoAuth: true,
}; };
async function init() { async function init() {
const spec = await loadAndBundleSpec(specUrl); const spec = await loadAndBundleSpec(specUrl);
store = new AppStore(spec, specUrl, options); store = new AppStore(spec, specUrl, options, true, markdown);
renderRoot({ store }); renderRoot({ store });
} }

View File

@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { HFlex } from '../common-elements/flex'; import { HFlex } from '../../../src/common-elements/flex';
import styled from 'styled-components'; import styled from '../../../src/styled-components';
const Button = styled.button` const Button = styled.button`
margin-left: auto; margin-left: auto;

View File

@ -4,17 +4,17 @@
```bash ```bash
#!/bin/bash #!/bin/bash
BSDEX_API_KEY=put-your-api-key-here YOUR_API_KEY=put-your-api-key-here
BSDEX_API_SECRET=put-your-api-secret-here YOUR_SECRET=put-your-api-secret-here
DATE="`date -u '+%a, %d %b %Y %T %Z'`" DATE="`date -u '+%a, %d %b %Y %T %Z'`"
# The signature is in fact HMAC signature of a few fields, currently only "date", using your API secret. # The signature is in fact HMAC signature of a few fields, currently only "date", using your API secret.
AUTHORIZATION="hmac username=\"$BSDEX_API_KEY\", algorithm=\"hmac-sha1\", headers=\"date\", signature=\"`/bin/echo -n "date: ${DATE}" | openssl sha1 -binary -hmac "${BSDEX_API_SECRET}" | base64 `\"" AUTHORIZATION="hmac username=\"$YOUR_API_KEY\", algorithm=\"hmac-sha1\", headers=\"date\", signature=\"`/bin/echo -n "date: ${DATE}" | openssl sha1 -binary -hmac "${YOUR_API_SECRET}" | base64 `\""
curl -v \ curl -v \
-H "Date: $DATE" \ -H "Date: $DATE" \
-H "ApiKey: $BSDEX_API_KEY" \ -H "ApiKey: $YOUR_API_KEY" \
-H "Authorization: $AUTHORIZATION" \ -H "Authorization: $AUTHORIZATION" \
-X GET "https://api-public.prelive.cex.tribe.sh/api/v1/balance" -X GET "https://example.com/api/v1/pets"
``` ```
> And you'll receive a response such as: > And you'll receive a response such as:
@ -22,14 +22,10 @@ curl -v \
```json ```json
[ [
{ {
"asset_id": "btc", "foo": "bar"
"available": "0",
"locked": "0"
}, },
{ {
"asset_id": "eur", "bar": "foo"
"available": "34163",
"locked": "123"
} }
] ]
``` ```
@ -49,7 +45,7 @@ where `signed_date` is the base64-encoded HMAC-SHA1 signature for the expression
Have a look at the code example to understand the HMAC signing process better. Have a look at the code example to understand the HMAC signing process better.
<aside class="notice"> <aside className="notice">
It is important to note that the value of the <em>Date</em> header must match the signed date value. It is important to note that the value of the <em>Date</em> header must match the signed date value.
</aside> </aside>

View File

@ -0,0 +1,15 @@
import { loadMarkdownIndexFromComponents } from '../../../src/markdown';
export const loadMarkdownIndex = (names) => {
const mdxComponents = names.map((include) => require(`./${include}.mdx`));
return loadMarkdownIndexFromComponents(mdxComponents)
};
const NAMES = [
'welcome',
'auth',
]
export default loadMarkdownIndex(NAMES)

View File

@ -0,0 +1,3 @@
{
"names": ["welcome", "auth"]
}

View File

@ -26,14 +26,7 @@ info:
This API features Cross-Origin Resource Sharing (CORS) implemented in compliance with [W3C spec](https://www.w3.org/TR/cors/). This API features Cross-Origin Resource Sharing (CORS) implemented in compliance with [W3C spec](https://www.w3.org/TR/cors/).
And that allows cross-domain communication from the browser. And that allows cross-domain communication from the browser.
All responses have a wildcard same-origin which makes them completely public and accessible to everyone, including any code on any site. All responses have a wildcard same-origin which makes them completely public and accessible to everyone, including any code on any site.
# Authentication
Petstore offers two forms of authentication:
- API Key
- OAuth2
OAuth2 - an open protocol to allow secure authorization in a simple
and standard method from web, mobile and desktop applications.
<!-- ReDoc-Inject: <security-definitions> -->
version: 1.0.0 version: 1.0.0
title: Swagger Petstore title: Swagger Petstore
termsOfService: 'http://swagger.io/terms/' termsOfService: 'http://swagger.io/terms/'

View File

@ -28,7 +28,7 @@ const tsLoader = (env) => ({
}, },
}); });
const babelLoader = (mode) => ({ const babelLoader = (mode, transformJsx = false) => ({
loader: 'babel-loader', loader: 'babel-loader',
options: { options: {
generatorOpts: { generatorOpts: {
@ -38,7 +38,7 @@ const babelLoader = (mode) => ({
['@babel/plugin-syntax-typescript', { isTSX: true }], ['@babel/plugin-syntax-typescript', { isTSX: true }],
['@babel/plugin-syntax-decorators', { legacy: true }], ['@babel/plugin-syntax-decorators', { legacy: true }],
'@babel/plugin-syntax-dynamic-import', '@babel/plugin-syntax-dynamic-import',
'@babel/plugin-transform-react-jsx', ...(transformJsx ? ['@babel/plugin-transform-react-jsx'] : []),
'@babel/plugin-syntax-jsx', '@babel/plugin-syntax-jsx',
[ [
'babel-plugin-styled-components', 'babel-plugin-styled-components',
@ -114,7 +114,7 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) =
{ {
test: /\.mdx?$/, test: /\.mdx?$/,
use: [ use: [
babelLoader(mode), babelLoader(mode, true),
{ {
loader: '@mdx-js/loader', loader: '@mdx-js/loader',
options: { options: {

22
package-lock.json generated
View File

@ -2366,9 +2366,9 @@
} }
}, },
"@types/react-native": { "@types/react-native": {
"version": "0.63.2", "version": "0.63.9",
"resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.63.2.tgz", "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.63.9.tgz",
"integrity": "sha512-oxbp084lUsZvwfdWmWxKjJAuqEraQDRf+cE/JgwmrHQMguSrmgIHZ3xkeoQ5FYnW5NHIPpHudB3BbjL1Zn3vnA==", "integrity": "sha512-6ec/z9zjAkFH3rD1RYqbrA/Lj+jux6bumWCte4yRy3leyelTdqtmOd2Ph+86IXQQzsIArEMBwmraAbNQ0J3UAA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/react": "*" "@types/react": "*"
@ -2408,15 +2408,23 @@
"dev": true "dev": true
}, },
"@types/styled-components": { "@types/styled-components": {
"version": "5.1.1", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.1.tgz", "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.2.tgz",
"integrity": "sha512-fIjKvDU1LJExBZWEQilHqzfpOK4KUwBsj5zC79lxa94ekz8oDQSBNcayMACBImxIuevF+NbBGL9O/2CQ67Zhig==", "integrity": "sha512-HNocYLfrsnNNm8NTS/W53OERSjRA8dx5Bn6wBd2rXXwt4Z3s+oqvY6/PbVt3e6sgtzI63GX//WiWiRhWur08qQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/hoist-non-react-statics": "*", "@types/hoist-non-react-statics": "*",
"@types/react": "*", "@types/react": "*",
"@types/react-native": "*", "@types/react-native": "*",
"csstype": "^2.2.0" "csstype": "^3.0.2"
},
"dependencies": {
"csstype": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.2.tgz",
"integrity": "sha512-ofovWglpqoqbfLNOTBNZLSbMuGrblAf1efvvArGKOZMBrIoJeu5UsAipQolkijtyQx5MtAzT/J9IHj/CEY1mJw==",
"dev": true
}
} }
}, },
"@types/tapable": { "@types/tapable": {

View File

@ -1,6 +1,12 @@
import styled from 'styled-components' import styled from '../styled-components';
export const Flex = styled.div` type FlexProps = {
justifyContent?: string;
};
export const Flex = styled.div.attrs((props: FlexProps) => ({
justifyContent: props.justifyContent,
}))<FlexProps>`
display: flex; display: flex;
justify-content: ${(props) => props.justifyContent}; justify-content: ${(props) => props.justifyContent};
width: 100%; width: 100%;

View File

@ -9,3 +9,4 @@ export * from './mixins';
export * from './tabs'; export * from './tabs';
export * from './samples'; export * from './samples';
export * from './perfect-scrollbar'; export * from './perfect-scrollbar';
export * as Flex from './flex';

View File

@ -1,10 +1,9 @@
import * as React from 'react'; import * as React from 'react';
import PerfectScrollbarType, * as PerfectScrollbarNamespace from 'perfect-scrollbar'; import PerfectScrollbarType, * as PerfectScrollbarNamespace from 'perfect-scrollbar';
import psStyles from 'perfect-scrollbar/css/perfect-scrollbar.css';
import { OptionsContext } from '../components/OptionsProvider'; import { OptionsContext } from '../components/OptionsProvider';
import styled, { createGlobalStyle } from '../styled-components'; import styled from '../styled-components';
/* /*
* perfect scrollbar umd bundle uses exports assignment while module uses default export * perfect scrollbar umd bundle uses exports assignment while module uses default export
@ -14,7 +13,7 @@ import styled, { createGlobalStyle } from '../styled-components';
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`${require('perfect-scrollbar/css/perfect-scrollbar.css').toString()}`;
const StyledScrollWrapper = styled.div` const StyledScrollWrapper = styled.div`
position: relative; position: relative;
@ -46,7 +45,7 @@ export class PerfectScrollbar extends React.Component<PerfectScrollbarProps> {
this.inst.destroy(); this.inst.destroy();
} }
handleRef = ref => { handleRef = (ref) => {
this._container = ref; this._container = ref;
}; };
@ -59,7 +58,7 @@ export class PerfectScrollbar extends React.Component<PerfectScrollbarProps> {
return ( return (
<> <>
<PSStyling /> {/* <PSStyling /> */}
<StyledScrollWrapper className={`scrollbar-container ${className}`} ref={this.handleRef}> <StyledScrollWrapper className={`scrollbar-container ${className}`} ref={this.handleRef}>
{children} {children}
</StyledScrollWrapper> </StyledScrollWrapper>
@ -73,7 +72,7 @@ export function PerfectScrollbarWrap(
) { ) {
return ( return (
<OptionsContext.Consumer> <OptionsContext.Consumer>
{options => {(options) =>
!options.nativeScrollbars ? ( !options.nativeScrollbars ? (
<PerfectScrollbar {...props}>{props.children}</PerfectScrollbar> <PerfectScrollbar {...props}>{props.children}</PerfectScrollbar>
) : ( ) : (

View File

@ -15,8 +15,7 @@ import { ApiContentWrap, BackgroundStub, RedocWrap } from './styled.elements';
import { SearchBox } from '../SearchBox/SearchBox'; import { SearchBox } from '../SearchBox/SearchBox';
import { StoreProvider } from '../StoreBuilder'; import { StoreProvider } from '../StoreBuilder';
import components from '../../markdown/components';
import { sections, components } from '../../markdown';
export interface RedocProps { export interface RedocProps {
store: AppStore; store: AppStore;
@ -37,9 +36,12 @@ export class Redoc extends React.Component<RedocProps> {
render() { render() {
const { const {
store: { spec, menu, options, search, marker }, store: { spec, menu, options, search, marker, markdownIndex },
} = this.props; } = this.props;
const store = this.props.store; const store = this.props.store;
console.log('components', components);
return ( return (
<ThemeProvider theme={options.theme}> <ThemeProvider theme={options.theme}>
<StoreProvider value={this.props.store}> <StoreProvider value={this.props.store}>
@ -61,9 +63,10 @@ export class Redoc extends React.Component<RedocProps> {
</StickyResponsiveSidebar> </StickyResponsiveSidebar>
<ApiContentWrap className="api-content"> <ApiContentWrap className="api-content">
<ApiInfo store={store} /> <ApiInfo store={store} />
{sections.map((MDXComponent, idx) => { {markdownIndex &&
return <MDXComponent key={`section-${idx}`} />; markdownIndex.components.map((MDXComponent, idx) => {
})} return <MDXComponent key={`mdxsection-${idx}`} />;
})}
<ContentItems items={menu.items as any} /> <ContentItems items={menu.items as any} />
</ApiContentWrap> </ApiContentWrap>
<BackgroundStub /> <BackgroundStub />

View File

@ -6,11 +6,13 @@ import { ErrorBoundary } from './ErrorBoundary';
import { Loading } from './Loading/Loading'; import { Loading } from './Loading/Loading';
import { Redoc } from './Redoc/Redoc'; import { Redoc } from './Redoc/Redoc';
import { StoreBuilder } from './StoreBuilder'; import { StoreBuilder } from './StoreBuilder';
import { MarkdownIndex } from '../markdown';
export interface RedocStandaloneProps { export interface RedocStandaloneProps {
spec?: object; spec?: object;
specUrl?: string; specUrl?: string;
options?: RedocRawOptions; options?: RedocRawOptions;
markdownIndex?: MarkdownIndex;
onLoaded?: (e?: Error) => any; onLoaded?: (e?: Error) => any;
} }

View File

@ -12,7 +12,7 @@ export class SelectOnClick extends React.PureComponent {
const { children } = this.props; const { children } = this.props;
return ( return (
<div <div
ref={el => (this.child = el)} ref={el => (this.child = (el as HTMLDivElement))}
onClick={this.selectElement} onClick={this.selectElement}
onFocus={this.selectElement} onFocus={this.selectElement}
tabIndex={0} tabIndex={0}

View File

@ -1,8 +1,9 @@
export * from './components'; export * from './components';
export { MiddlePanel, Row, RightPanel, Section } from './common-elements/'; export { MiddlePanel, Row, RightPanel, Section, Flex } from './common-elements/';
export { OpenAPIEncoding } from './types'; export { OpenAPIEncoding } from './types';
export * from './services'; export * from './services';
export * from './utils'; export * from './utils';
export * from './styled-components'; export * from './styled-components';
export { default as styled } from './styled-components'; export { default as styled } from './styled-components';
export * from './markdown';

View File

@ -1,17 +1,20 @@
import * as React from 'react'; import * as React from 'react';
import Highlight, { defaultProps, Language } from 'prism-react-renderer'; import Highlight, { defaultProps, Language } from 'prism-react-renderer';
import styled from 'styled-components'; import styled from '../../../styled-components';
type Props = { type Props = {
className: string; className: string;
style: any;
componentId?: string;
}; };
const OverflowHighlighter = styled.pre` const OverflowHighlighter = styled.pre`
overflow-x: scroll; overflow-x: scroll;
`; `;
const Highligher: React.FC<Props> = ({ children, className }) => { const Highlighter: React.FC<Props> = ({ children, className }) => {
const language = className.replace(/language-/, ''); console.log('className', className);
const language = className ? className.replace(/language-/, '').trim() : 'bash';
if (!children) { if (!children) {
return null; return null;
@ -34,4 +37,4 @@ const Highligher: React.FC<Props> = ({ children, className }) => {
); );
}; };
export default Highligher; export default Highlighter;

View File

@ -0,0 +1,89 @@
import * as React from 'react';
import {
DarkRightPanel,
H1,
H2,
MiddlePanel,
PropertiesTable,
Row,
Section,
ShareLink,
} from '../common-elements';
import Highlighter from './code/Highlighter';
const H1Comp = ({ children, ...props }) => {
return (
<H1 id={props.id}>
<ShareLink to={props.id as string} />
{children}
</H1>
);
};
const H2Comp = ({ children, ...props }) => (
<H2 id={props.id}>
<ShareLink to={props.id as string} />
{children}
</H2>
);
const Wrapper = ({ children }) => {
const getSections = (children) => {
let currentSection: any[] = [];
let currentId: number | null = null;
let currentCodeBlocks: any[] = [];
const result: any[] = [];
children.forEach((child) => {
const type = child.props.mdxType;
if (['h1', 'h2', 'h3', 'h4', 'h5'].includes(type)) {
if (currentSection.length > 0) {
result.push({
id: currentId,
middle: currentSection,
right: currentCodeBlocks,
});
}
currentId = child.props.id;
currentSection = [];
currentCodeBlocks = [];
}
if (['code', 'pre', 'blockquote'].includes(type)) {
currentCodeBlocks.push(child);
} else {
currentSection.push(child);
}
});
if (currentSection.length > 0) {
result.push({ id: currentId, middle: currentSection, right: currentCodeBlocks });
}
return result;
};
const sections = getSections(children);
console.log('sections', sections);
return (
<>
{sections.map((section) => (
<Section key={`resection-${section.id}`} id={section.id}>
<Row id={section.id}>
{section.middle && <MiddlePanel>{section.middle}</MiddlePanel>}
{section.right && <DarkRightPanel>{section.right}</DarkRightPanel>}
</Row>
</Section>
))}
</>
);
};
export default {
code: Highlighter,
h1: H1Comp,
h2: H2Comp,
table: PropertiesTable,
wrapper: Wrapper,
};

View File

@ -1,100 +1,30 @@
import * as React from 'react'; const TocPlugin = require('./plugins/export-toc');
import {
DarkRightPanel,
H1,
H2,
MiddlePanel,
PropertiesTable,
Row,
Section,
ShareLink,
} from '../common-elements';
import Highlighter from './code/Highlighter';
const INCLUDES = ['auth', 'welcome']; type Header = {
depth: number;
const H1Comp = ({ children, ...props }) => { id: number;
return ( text: string;
<H1 id={props.id}> };
<ShareLink to={props.id as string} /> export type MarkdownDocument = {
{children} component: (props: any) => JSX.Element;
</H1> headings: Header[];
);
}; };
const H2Comp = ({ children, ...props }) => ( export type MarkdownIndex = {
<H2 id={props.id}> components: ((props: any) => JSX.Element)[];
<ShareLink to={props.id as string} /> headings: Header[];
{children} };
</H2>
);
const Wrapper = ({ children }) => { export const loadMarkdownIndexFromComponents = (mdxComponents) => {
const getSections = (children) => { const components = mdxComponents.map((sect) => sect.default);
let currentSection: any[] = []; const headings = mdxComponents.reduce((acc, section) => [...acc, ...section.headings], []);
let currentId: number | null = null;
let currentCodeBlocks: any[] = [];
const result: any[] = [];
children.forEach((child) => { return {
const type = child.props.mdxType; components,
if (['h1', 'h2', 'h3', 'h4', 'h5'].includes(type)) { headings,
if (currentSection.length > 0) {
result.push({
id: currentId,
middle: currentSection,
right: currentCodeBlocks,
});
}
currentId = child.props.id;
currentSection = [];
currentCodeBlocks = [];
}
if (['code', 'pre', 'blockquote'].includes(type)) {
currentCodeBlocks.push(child);
} else {
currentSection.push(child);
}
});
if (currentSection.length > 0) {
result.push({ id: currentId, middle: currentSection, right: currentCodeBlocks });
}
return result;
}; };
const sections = getSections(children);
return (
<>
{sections.map((section) => (
<Section key={`section-${section.id}`} id={section.id}>
<Row id={section.id}>
{section.middle && <MiddlePanel>{section.middle}</MiddlePanel>}
{section.right && <DarkRightPanel>{section.right}</DarkRightPanel>}
</Row>
</Section>
))}
</>
);
}; };
const includedSections = INCLUDES.map((include) => require(`./${include}.mdx`)); export default {
remarkPlugins: [TocPlugin],
export const sections = includedSections.map((sect) => sect.default);
export const headings = includedSections.reduce(
(acc, section) => [...acc, ...section.headings],
[],
);
export const components = {
code: Highlighter,
h1: H1Comp,
h2: H2Comp,
table: PropertiesTable,
wrapper: Wrapper,
}; };
export default sections;

View File

@ -19,26 +19,29 @@ import {
} from '../utils/openapi'; } from '../utils/openapi';
import { IS_BROWSER } from '../utils'; import { IS_BROWSER } from '../utils';
import { MarkdownIndex } from '../markdown';
export interface StoreState { export interface StoreState {
menu: { menu: {
activeItemIdx: number; activeItemIdx: number;
}; };
spec: { spec: {
url?: string; url?: string | null;
data: any; data: any;
}; };
searchIndex: any; searchIndex: any;
options: RedocRawOptions; options: RedocRawOptions;
markdownIndex?: MarkdownIndex;
} }
export async function createStore( export async function createStore(
spec: object, spec: object,
specUrl: string | undefined, specUrl: string | undefined | null,
options: RedocRawOptions = {}, options: RedocRawOptions = {},
markdownIndex?: MarkdownIndex,
) { ) {
const resolvedSpec = await loadAndBundleSpec(spec || specUrl); const resolvedSpec = await loadAndBundleSpec(spec || specUrl);
return new AppStore(resolvedSpec, specUrl, options); return new AppStore(resolvedSpec, specUrl, options, true, markdownIndex);
} }
export class AppStore { export class AppStore {
@ -47,8 +50,8 @@ export class AppStore {
* **SUPER HACKY AND NOT OPTIMAL IMPLEMENTATION** * **SUPER HACKY AND NOT OPTIMAL IMPLEMENTATION**
*/ */
// TODO: // TODO:
static fromJS(state: StoreState): AppStore { static fromJS(state: StoreState, markdownIndex?: MarkdownIndex): AppStore {
const inst = new AppStore(state.spec.data, state.spec.url, state.options, false); const inst = new AppStore(state.spec.data, state.spec.url, state.options, false, markdownIndex);
inst.menu.activeItemIdx = state.menu.activeItemIdx || 0; inst.menu.activeItemIdx = state.menu.activeItemIdx || 0;
inst.menu.activate(inst.menu.flatItems[inst.menu.activeItemIdx]); inst.menu.activate(inst.menu.flatItems[inst.menu.activeItemIdx]);
if (!inst.options.disableSearch) { if (!inst.options.disableSearch) {
@ -63,15 +66,17 @@ export class AppStore {
options: RedocNormalizedOptions; options: RedocNormalizedOptions;
search?: SearchStore<string>; search?: SearchStore<string>;
marker = new MarkerService(); marker = new MarkerService();
markdownIndex?: MarkdownIndex;
private scroll: ScrollService; private scroll: ScrollService;
private disposer: Lambda | null = null; private disposer: Lambda | null = null;
constructor( constructor(
spec: OpenAPISpec, spec: OpenAPISpec,
specUrl?: string, specUrl?: string | null,
options: RedocRawOptions = {}, options: RedocRawOptions = {},
createSearchIndex: boolean = true, createSearchIndex: boolean = true,
markdownIndex?: MarkdownIndex,
) { ) {
this.rawOptions = options; this.rawOptions = options;
this.options = new RedocNormalizedOptions(options, DEFAULT_OPTIONS); this.options = new RedocNormalizedOptions(options, DEFAULT_OPTIONS);
@ -81,7 +86,9 @@ export class AppStore {
MenuStore.updateOnHistory(history.currentId, this.scroll); MenuStore.updateOnHistory(history.currentId, this.scroll);
this.spec = new SpecStore(spec, specUrl, this.options); this.spec = new SpecStore(spec, specUrl, this.options);
this.menu = new MenuStore(this.spec, this.scroll, history);
this.menu = new MenuStore(this.spec, this.scroll, history, markdownIndex);
this.markdownIndex = markdownIndex;
if (!this.options.disableSearch) { if (!this.options.disableSearch) {
this.search = new SearchStore(); this.search = new SearchStore();
@ -122,11 +129,12 @@ export class AppStore {
activeItemIdx: this.menu.activeItemIdx, activeItemIdx: this.menu.activeItemIdx,
}, },
spec: { spec: {
url: this.spec.parser.specUrl, url: this.spec.parser.specUrl || null,
data: this.spec.parser.spec, data: this.spec.parser.spec,
}, },
searchIndex: this.search ? await this.search.toJS() : undefined, searchIndex: this.search ? await this.search.toJS() : undefined,
options: this.rawOptions, options: this.rawOptions,
markdownIndex: this.markdownIndex,
}; };
} }

View File

@ -6,7 +6,7 @@ import { GROUP_DEPTH } from './MenuBuilder';
import { SpecStore } from './models'; import { SpecStore } from './models';
import { ScrollService } from './ScrollService'; import { ScrollService } from './ScrollService';
import { ExtraContent } from './models/ExtraContent'; import { ExtraContent } from './models/ExtraContent';
import { headings } from '../markdown'; import { MarkdownIndex } from '../markdown';
export type MenuItemGroupType = 'group' | 'tag' | 'section' | 'extra'; export type MenuItemGroupType = 'group' | 'tag' | 'section' | 'extra';
export type MenuItemType = MenuItemGroupType | 'operation'; export type MenuItemType = MenuItemGroupType | 'operation';
@ -38,246 +38,252 @@ export const SECTION_ATTR = 'data-section-id';
* Stores all side-menu related information * Stores all side-menu related information
*/ */
export class MenuStore { export class MenuStore {
/** /**
* Statically try update scroll position * Statically try update scroll position
* Used before hydrating from server-side rendered html to scroll page faster * Used before hydrating from server-side rendered html to scroll page faster
*/ */
static updateOnHistory(id: string = historyInst.currentId, scroll: ScrollService) { static updateOnHistory(id: string = historyInst.currentId, scroll: ScrollService) {
if (!id) { if (!id) {
return; return;
} }
scroll.scrollIntoViewBySelector(`[${SECTION_ATTR}="${id}"]`); scroll.scrollIntoViewBySelector(`[${SECTION_ATTR}="${id}"]`);
} }
/** /**
* active item absolute index (when flattened). -1 means nothing is selected * active item absolute index (when flattened). -1 means nothing is selected
*/ */
@observable @observable
activeItemIdx: number = -1; activeItemIdx: number = -1;
/** /**
* whether sidebar with menu is opened or not * whether sidebar with menu is opened or not
*/ */
@observable @observable
sideBarOpened: boolean = false; sideBarOpened: boolean = false;
items: IMenuItem[]; items: IMenuItem[];
flatItems: IMenuItem[]; flatItems: IMenuItem[];
/** /**
* cached flattened menu items to support absolute indexing * cached flattened menu items to support absolute indexing
*/ */
private _unsubscribe: () => void; private _unsubscribe: () => void;
private _hashUnsubscribe: () => void; private _hashUnsubscribe: () => void;
/** /**
* *
* @param spec [SpecStore](#SpecStore) which contains page content structure * @param spec [SpecStore](#SpecStore) which contains page content structure
* @param scroll scroll service instance used by this menu * @param scroll scroll service instance used by this menu
*/ */
constructor(spec: SpecStore, public scroll: ScrollService, public history: HistoryService) { constructor(
const extraItems = headings.map( spec: SpecStore,
({ id, text, depth }) => public scroll: ScrollService,
new ExtraContent({ public history: HistoryService,
id, markdownIndex?: MarkdownIndex,
name: text, ) {
depth, const { headings: extraHeadings } = markdownIndex || {};
}), const extraItems = extraHeadings ? extraHeadings.map(
); ({ id, text, depth }) =>
this.items = [...extraItems, ...spec.contentItems]; new ExtraContent({
id,
name: text,
depth,
})
) : [];
this.items = [...extraItems, ...spec.contentItems];
this.flatItems = flattenByProp(this.items || [], 'items'); this.flatItems = flattenByProp(this.items || [], 'items');
this.flatItems.forEach((item, idx) => (item.absoluteIdx = idx)); this.flatItems.forEach((item, idx) => (item.absoluteIdx = idx));
this.subscribe(); this.subscribe();
} }
subscribe() { subscribe() {
this._unsubscribe = this.scroll.subscribe(this.updateOnScroll); this._unsubscribe = this.scroll.subscribe(this.updateOnScroll);
this._hashUnsubscribe = this.history.subscribe(this.updateOnHistory); this._hashUnsubscribe = this.history.subscribe(this.updateOnHistory);
} }
@action @action
toggleSidebar() { toggleSidebar() {
this.sideBarOpened = this.sideBarOpened ? false : true; this.sideBarOpened = this.sideBarOpened ? false : true;
} }
@action @action
closeSidebar() { closeSidebar() {
this.sideBarOpened = false; this.sideBarOpened = false;
} }
/** /**
* update active items on scroll * update active items on scroll
* @param isScrolledDown whether last scroll was downside * @param isScrolledDown whether last scroll was downside
*/ */
updateOnScroll = (isScrolledDown: boolean): void => { updateOnScroll = (isScrolledDown: boolean): void => {
const step = isScrolledDown ? 1 : -1; const step = isScrolledDown ? 1 : -1;
let itemIdx = this.activeItemIdx; let itemIdx = this.activeItemIdx;
while (true) { while (true) {
if (itemIdx === -1 && !isScrolledDown) { if (itemIdx === -1 && !isScrolledDown) {
break; break;
} }
if (itemIdx >= this.flatItems.length - 1 && isScrolledDown) { if (itemIdx >= this.flatItems.length - 1 && isScrolledDown) {
break; break;
} }
if (isScrolledDown) { if (isScrolledDown) {
const el = this.getElementAtOrFirstChild(itemIdx + 1); const el = this.getElementAtOrFirstChild(itemIdx + 1);
if (this.scroll.isElementBellow(el)) { if (this.scroll.isElementBellow(el)) {
break; break;
} }
} else { } else {
const el = this.getElementAt(itemIdx); const el = this.getElementAt(itemIdx);
if (this.scroll.isElementAbove(el)) { if (this.scroll.isElementAbove(el)) {
break; break;
} }
} }
itemIdx += step; itemIdx += step;
} }
this.activate(this.flatItems[itemIdx], true, true); this.activate(this.flatItems[itemIdx], true, true);
}; };
/** /**
* update active items on hash change * update active items on hash change
* @param id current hash * @param id current hash
*/ */
updateOnHistory = (id: string = this.history.currentId) => { updateOnHistory = (id: string = this.history.currentId) => {
if (!id) { if (!id) {
return; return;
} }
let item: IMenuItem | undefined; let item: IMenuItem | undefined;
item = this.flatItems.find((i) => i.id === id); item = this.flatItems.find((i) => i.id === id);
if (item) { if (item) {
this.activateAndScroll(item, false); this.activateAndScroll(item, false);
} else { } else {
if (id.startsWith(SECURITY_SCHEMES_SECTION_PREFIX)) { if (id.startsWith(SECURITY_SCHEMES_SECTION_PREFIX)) {
item = this.flatItems.find((i) => SECURITY_SCHEMES_SECTION_PREFIX.startsWith(i.id)); item = this.flatItems.find((i) => SECURITY_SCHEMES_SECTION_PREFIX.startsWith(i.id));
this.activate(item); this.activate(item);
} }
this.scroll.scrollIntoViewBySelector(`[${SECTION_ATTR}="${id}"]`); this.scroll.scrollIntoViewBySelector(`[${SECTION_ATTR}="${id}"]`);
} }
}; };
/** /**
* get section/operation DOM Node related to the item or null if it doesn't exist * get section/operation DOM Node related to the item or null if it doesn't exist
* @param idx item absolute index * @param idx item absolute index
*/ */
getElementAt(idx: number): Element | null { getElementAt(idx: number): Element | null {
const item = this.flatItems[idx]; const item = this.flatItems[idx];
return (item && querySelector(`[${SECTION_ATTR}="${item.id}"]`)) || null; return (item && querySelector(`[${SECTION_ATTR}="${item.id}"]`)) || null;
} }
/** /**
* get section/operation DOM Node related to the item or if it is group item, returns first item of the group * get section/operation DOM Node related to the item or if it is group item, returns first item of the group
* @param idx item absolute index * @param idx item absolute index
*/ */
getElementAtOrFirstChild(idx: number): Element | null { getElementAtOrFirstChild(idx: number): Element | null {
let item = this.flatItems[idx]; let item = this.flatItems[idx];
if (item && item.type === 'group') { if (item && item.type === 'group') {
item = item.items[0]; item = item.items[0];
} }
return (item && querySelector(`[${SECTION_ATTR}="${item.id}"]`)) || null; return (item && querySelector(`[${SECTION_ATTR}="${item.id}"]`)) || null;
} }
/** /**
* current active item * current active item
*/ */
get activeItem(): IMenuItem { get activeItem(): IMenuItem {
return this.flatItems[this.activeItemIdx] || undefined; return this.flatItems[this.activeItemIdx] || undefined;
} }
getItemById = (id: string) => { getItemById = (id: string) => {
return this.flatItems.find((item) => item.id === id); return this.flatItems.find((item) => item.id === id);
}; };
/** /**
* activate menu item * activate menu item
* @param item item to activate * @param item item to activate
* @param updateLocation [true] whether to update location * @param updateLocation [true] whether to update location
* @param rewriteHistory [false] whether to rewrite browser history (do not create new entry) * @param rewriteHistory [false] whether to rewrite browser history (do not create new entry)
*/ */
@action @action
activate( activate(
item: IMenuItem | undefined, item: IMenuItem | undefined,
updateLocation: boolean = true, updateLocation: boolean = true,
rewriteHistory: boolean = false, rewriteHistory: boolean = false,
) { ) {
if ((this.activeItem && this.activeItem.id) === (item && item.id)) { if ((this.activeItem && this.activeItem.id) === (item && item.id)) {
return; return;
} }
if (item && item.type === 'group') { if (item && item.type === 'group') {
return; return;
} }
this.deactivate(this.activeItem); this.deactivate(this.activeItem);
if (!item) { if (!item) {
this.history.replace('', rewriteHistory); this.history.replace('', rewriteHistory);
return; return;
} }
// do not allow activating group items // do not allow activating group items
// TODO: control over options // TODO: control over options
if (item.depth <= GROUP_DEPTH) { if (item.depth <= GROUP_DEPTH) {
return; return;
} }
this.activeItemIdx = item.absoluteIdx!; this.activeItemIdx = item.absoluteIdx!;
if (updateLocation) { if (updateLocation) {
this.history.replace(item.id, rewriteHistory); this.history.replace(item.id, rewriteHistory);
} }
item.activate(); item.activate();
item.expand(); item.expand();
} }
/** /**
* makes item and all the parents not active * makes item and all the parents not active
* @param item item to deactivate * @param item item to deactivate
*/ */
deactivate(item: IMenuItem | undefined) { deactivate(item: IMenuItem | undefined) {
if (item === undefined) { if (item === undefined) {
return; return;
} }
item.deactivate(); item.deactivate();
while (item !== undefined) { while (item !== undefined) {
item.collapse(); item.collapse();
item = item.parent; item = item.parent;
} }
} }
/** /**
* activate menu item and scroll to it * activate menu item and scroll to it
* @see MenuStore.activate * @see MenuStore.activate
*/ */
@action.bound @action.bound
activateAndScroll( activateAndScroll(
item: IMenuItem | undefined, item: IMenuItem | undefined,
updateLocation?: boolean, updateLocation?: boolean,
rewriteHistory?: boolean, rewriteHistory?: boolean,
) { ) {
// item here can be a copy from search results so find corresponding item from menu // item here can be a copy from search results so find corresponding item from menu
const menuItem = (item && this.getItemById(item.id)) || item; const menuItem = (item && this.getItemById(item.id)) || item;
this.activate(menuItem, updateLocation, rewriteHistory); this.activate(menuItem, updateLocation, rewriteHistory);
this.scrollToActive(); this.scrollToActive();
if (!menuItem || !menuItem.items.length) { if (!menuItem || !menuItem.items.length) {
this.closeSidebar(); this.closeSidebar();
} }
} }
/** /**
* scrolls to active section * scrolls to active section
*/ */
scrollToActive(): void { scrollToActive(): void {
this.scroll.scrollIntoView(this.getElementAt(this.activeItemIdx)); this.scroll.scrollIntoView(this.getElementAt(this.activeItemIdx));
} }
dispose() { dispose() {
this._unsubscribe(); this._unsubscribe();
this._hashUnsubscribe(); this._hashUnsubscribe();
} }
} }

View File

@ -2,7 +2,7 @@ import { resolve as urlResolve } from 'url';
import { OpenAPIRef, OpenAPISchema, OpenAPISpec, Referenced } from '../types'; import { OpenAPIRef, OpenAPISchema, OpenAPISpec, Referenced } from '../types';
import { appendToMdHeading, IS_BROWSER } from '../utils/'; import { appendToMdHeading } from '../utils/';
import { JsonPointer } from '../utils/JsonPointer'; import { JsonPointer } from '../utils/JsonPointer';
import { import {
isNamedDefinition, isNamedDefinition,
@ -42,7 +42,7 @@ class RefCounter {
* Loads and keeps spec. Provides raw spec operations * Loads and keeps spec. Provides raw spec operations
*/ */
export class OpenAPIParser { export class OpenAPIParser {
specUrl?: string; specUrl?: string | null;
spec: OpenAPISpec; spec: OpenAPISpec;
mergeRefs: Set<string>; mergeRefs: Set<string>;
@ -50,7 +50,7 @@ export class OpenAPIParser {
constructor( constructor(
spec: OpenAPISpec, spec: OpenAPISpec,
specUrl?: string, specUrl?: string | null,
private options: RedocNormalizedOptions = new RedocNormalizedOptions({}), private options: RedocNormalizedOptions = new RedocNormalizedOptions({}),
) { ) {
this.validate(spec); this.validate(spec);
@ -60,7 +60,7 @@ export class OpenAPIParser {
this.mergeRefs = new Set(); this.mergeRefs = new Set();
const href = IS_BROWSER ? window.location.href : ''; const href = '';
if (typeof specUrl === 'string') { if (typeof specUrl === 'string') {
this.specUrl = urlResolve(href, specUrl); this.specUrl = urlResolve(href, specUrl);
} }
@ -214,7 +214,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;
} }
@ -228,7 +228,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;
}>; }>;
@ -305,7 +305,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];
} }
@ -331,7 +331,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

@ -20,7 +20,7 @@ export class SpecStore {
constructor( constructor(
spec: OpenAPISpec, spec: OpenAPISpec,
specUrl: string | undefined, specUrl: string | undefined | null,
private options: RedocNormalizedOptions, private options: RedocNormalizedOptions,
) { ) {
this.parser = new OpenAPIParser(spec, specUrl, options); this.parser = new OpenAPIParser(spec, specUrl, options);

View File

@ -13,7 +13,6 @@ import {
OpenAPIServer, OpenAPIServer,
Referenced, Referenced,
} from '../types'; } from '../types';
import { IS_BROWSER } from './dom';
import { isNumeric, removeQueryString, resolveUrl } from './helpers'; import { isNumeric, removeQueryString, resolveUrl } from './helpers';
function isWildcardStatusCode(statusCode: string | number): statusCode is string { function isWildcardStatusCode(statusCode: string | number): statusCode is string {
@ -140,10 +139,10 @@ export function isFormUrlEncoded(contentType: string): boolean {
function delimitedEncodeField(fieldVal: any, fieldName: string, delimiter: string): string { function delimitedEncodeField(fieldVal: any, fieldName: string, delimiter: string): string {
if (Array.isArray(fieldVal)) { if (Array.isArray(fieldVal)) {
return fieldVal.map(v => v.toString()).join(delimiter); return fieldVal.map((v) => v.toString()).join(delimiter);
} else if (typeof fieldVal === 'object') { } else if (typeof fieldVal === 'object') {
return Object.keys(fieldVal) return Object.keys(fieldVal)
.map(k => `${k}${delimiter}${fieldVal[k]}`) .map((k) => `${k}${delimiter}${fieldVal[k]}`)
.join(delimiter); .join(delimiter);
} else { } else {
return fieldName + '=' + fieldVal.toString(); return fieldName + '=' + fieldVal.toString();
@ -156,7 +155,7 @@ function deepObjectEncodeField(fieldVal: any, fieldName: string): string {
return ''; return '';
} else if (typeof fieldVal === 'object') { } else if (typeof fieldVal === 'object') {
return Object.keys(fieldVal) return Object.keys(fieldVal)
.map(k => `${fieldName}[${k}]=${fieldVal[k]}`) .map((k) => `${fieldName}[${k}]=${fieldVal[k]}`)
.join('&'); .join('&');
} else { } else {
console.warn('deepObject style cannot be used with non-object value:' + fieldVal.toString()); console.warn('deepObject style cannot be used with non-object value:' + fieldVal.toString());
@ -188,7 +187,7 @@ export function urlFormEncodePayload(
throw new Error('Payload must have fields: ' + payload.toString()); throw new Error('Payload must have fields: ' + payload.toString());
} else { } else {
return Object.keys(payload) return Object.keys(payload)
.map(fieldName => { .map((fieldName) => {
const fieldVal = payload[fieldName]; const fieldVal = payload[fieldName];
const { style = 'form', explode = true } = encoding[fieldName] || {}; const { style = 'form', explode = true } = encoding[fieldName] || {};
switch (style) { switch (style) {
@ -450,7 +449,7 @@ export function sortByRequired(fields: FieldModel[], order: string[] = []) {
const orderedFields: FieldModel[] = []; const orderedFields: FieldModel[] = [];
const unorderedFields: FieldModel[] = []; const unorderedFields: FieldModel[] = [];
fields.forEach(field => { fields.forEach((field) => {
if (field.required) { if (field.required) {
order.includes(field.name) ? orderedFields.push(field) : unorderedFields.push(field); order.includes(field.name) ? orderedFields.push(field) : unorderedFields.push(field);
} else { } else {
@ -478,13 +477,13 @@ export function mergeParams(
operationParams: Array<Referenced<OpenAPIParameter>> = [], operationParams: Array<Referenced<OpenAPIParameter>> = [],
): Array<Referenced<OpenAPIParameter>> { ): Array<Referenced<OpenAPIParameter>> {
const operationParamNames = {}; const operationParamNames = {};
operationParams.forEach(param => { operationParams.forEach((param) => {
param = parser.shalowDeref(param); param = parser.shalowDeref(param);
operationParamNames[param.name + '_' + param.in] = true; operationParamNames[param.name + '_' + param.in] = true;
}); });
// filter out path params overridden by operation ones with the same name // filter out path params overridden by operation ones with the same name
pathParams = pathParams.filter(param => { pathParams = pathParams.filter((param) => {
param = parser.shalowDeref(param); param = parser.shalowDeref(param);
return !operationParamNames[param.name + '_' + param.in]; return !operationParamNames[param.name + '_' + param.in];
}); });
@ -496,7 +495,7 @@ export function mergeSimilarMediaTypes(
types: Record<string, OpenAPIMediaType>, types: Record<string, OpenAPIMediaType>,
): Record<string, OpenAPIMediaType> { ): Record<string, OpenAPIMediaType> {
const mergedTypes = {}; const mergedTypes = {};
Object.keys(types).forEach(name => { Object.keys(types).forEach((name) => {
const mime = types[name]; const mime = types[name];
// ignore content type parameters (e.g. charset) and merge // ignore content type parameters (e.g. charset) and merge
const normalizedMimeName = name.split(';')[0].trim(); const normalizedMimeName = name.split(';')[0].trim();
@ -518,18 +517,14 @@ export function expandDefaultServerVariables(url: string, variables: object = {}
} }
export function normalizeServers( export function normalizeServers(
specUrl: string | undefined, specUrl: string | undefined | null,
servers: OpenAPIServer[], servers: OpenAPIServer[],
): OpenAPIServer[] { ): OpenAPIServer[] {
const getHref = () => { const getHref = () => {
if (!IS_BROWSER) { return '';
return '';
}
const href = window.location.href;
return href.endsWith('.html') ? dirname(href) : href;
}; };
const baseUrl = specUrl === undefined ? removeQueryString(getHref()) : dirname(specUrl); const baseUrl = !specUrl ? removeQueryString(getHref()) : dirname(specUrl);
if (servers.length === 0) { if (servers.length === 0) {
// Behaviour defined in OpenAPI spec: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#openapi-object // Behaviour defined in OpenAPI spec: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#openapi-object
@ -544,7 +539,7 @@ export function normalizeServers(
return resolveUrl(baseUrl, url); return resolveUrl(baseUrl, url);
} }
return servers.map(server => { return servers.map((server) => {
return { return {
...server, ...server,
url: normalizeUrl(server.url), url: normalizeUrl(server.url),
@ -562,7 +557,7 @@ export function setSecuritySchemePrefix(prefix: string) {
SECURITY_SCHEMES_SECTION_PREFIX = prefix; SECURITY_SCHEMES_SECTION_PREFIX = prefix;
} }
export const shortenHTTPVerb = verb => export const shortenHTTPVerb = (verb) =>
({ ({
delete: 'del', delete: 'del',
options: 'opts', options: 'opts',
@ -593,7 +588,7 @@ export function extractExtensions(
showExtensions: string[] | true, showExtensions: string[] | true,
): Record<string, any> { ): Record<string, any> {
return Object.keys(obj) return Object.keys(obj)
.filter(key => { .filter((key) => {
if (showExtensions === true) { if (showExtensions === true) {
return key.startsWith('x-') && !isRedocExtension(key); return key.startsWith('x-') && !isRedocExtension(key);
} }
@ -608,6 +603,6 @@ export function extractExtensions(
export function pluralizeType(displayType: string): string { export function pluralizeType(displayType: string): string {
return displayType return displayType
.split(' or ') .split(' or ')
.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 ');
} }

View File

@ -4,6 +4,10 @@ import * as webpack from 'webpack';
import * as path from 'path'; import * as path from 'path';
const remarkSlugs = require('remark-slug');
const rehypeHtml = require('rehype-stringify');
const exportToc = require('./src/markdown/plugins/export-toc');
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)
allowlist: [ allowlist: [
@ -27,6 +31,28 @@ try {
console.error('Skipping REDOC_REVISION'); console.error('Skipping REDOC_REVISION');
} }
const babelLoader = (mode, transformJsx = false) => ({
loader: 'babel-loader',
options: {
generatorOpts: {
decoratorsBeforeExport: true,
},
plugins: [
['@babel/plugin-syntax-typescript', { isTSX: true }],
['@babel/plugin-syntax-decorators', { legacy: true }],
'@babel/plugin-syntax-jsx',
...(transformJsx ? ['@babel/plugin-transform-react-jsx'] : []),
[
'babel-plugin-styled-components',
{
minify: true,
displayName: mode !== 'production',
},
],
],
},
});
const BANNER = `ReDoc - OpenAPI/Swagger-generated API Reference Documentation const BANNER = `ReDoc - OpenAPI/Swagger-generated API Reference Documentation
------------------------------------------------------------- -------------------------------------------------------------
Version: ${VERSION} Version: ${VERSION}
@ -76,6 +102,19 @@ export default (env: { standalone?: boolean } = {}, { mode }) => ({
module: { module: {
rules: [ rules: [
{
test: /\.mdx?$/,
use: [
babelLoader(mode, true),
{
loader: '@mdx-js/loader',
options: {
remarkPlugins: [remarkSlugs, exportToc],
rehypePlugins: [rehypeHtml],
},
},
],
},
{ {
test: /\.tsx?$/, test: /\.tsx?$/,
use: [ use: [
@ -88,26 +127,7 @@ export default (env: { standalone?: boolean } = {}, { mode }) => ({
}, },
}, },
}, },
{ babelLoader(mode),
loader: 'babel-loader',
options: {
generatorOpts: {
decoratorsBeforeExport: true,
},
plugins: [
['@babel/plugin-syntax-typescript', { isTSX: true }],
['@babel/plugin-syntax-decorators', { legacy: true }],
'@babel/plugin-syntax-jsx',
[
'babel-plugin-styled-components',
{
minify: true,
displayName: mode !== 'production',
},
],
],
},
},
], ],
exclude: [/node_modules/], exclude: [/node_modules/],
}, },