do not crashe when used in node (isomorphic)

This commit is contained in:
Roman Hotsiy 2018-01-09 19:00:17 +02:00
parent 50e2a5868d
commit d67852f9c4
No known key found for this signature in database
GPG Key ID: 5CB7B3ACABA57CB0
18 changed files with 128 additions and 82 deletions

View File

@ -50,7 +50,6 @@
"enzyme": "^3.1.1",
"enzyme-adapter-react-16": "^1.0.4",
"enzyme-to-json": "^3.2.2",
"extract-text-webpack-plugin": "^3.0.0",
"html-webpack-plugin": "^2.30.1",
"http-server": "^0.10.0",
"jest": "^21.1.0",

View File

@ -1,8 +1,10 @@
import styled from '../styled-components';
import 'perfect-scrollbar/dist/css/perfect-scrollbar.css';
import styled, { injectGlobal } from '../styled-components';
import psStyles from 'perfect-scrollbar/dist/css/perfect-scrollbar.css';
import PerfectScrollbarOriginal from 'react-perfect-scrollbar';
injectGlobal`${psStyles}`;
export const PerfectScrollbar = styled(PerfectScrollbarOriginal)`
position: relative;
`;

View File

@ -72,7 +72,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"weight": "300",
},
"code": Object {
"fontFamily": "\\"Lucida Console\\", Monaco, monospace",
"fontFamily": "Courirer, monospace",
"fontSize": "13px",
},
"colors": Object {
@ -114,6 +114,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"width": 40,
},
"schemaView": Object {
"defaultDetailsWidth": "75%",
"linesColor": "#7f99cf",
},
"spacingUnit": 20,
@ -176,7 +177,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"weight": "300",
},
"code": Object {
"fontFamily": "\\"Lucida Console\\", Monaco, monospace",
"fontFamily": "Courirer, monospace",
"fontSize": "13px",
},
"colors": Object {
@ -218,6 +219,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"width": 40,
},
"schemaView": Object {
"defaultDetailsWidth": "75%",
"linesColor": "#7f99cf",
},
"spacingUnit": 20,
@ -263,7 +265,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"weight": "300",
},
"code": Object {
"fontFamily": "\\"Lucida Console\\", Monaco, monospace",
"fontFamily": "Courirer, monospace",
"fontSize": "13px",
},
"colors": Object {
@ -305,6 +307,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"width": 40,
},
"schemaView": Object {
"defaultDetailsWidth": "75%",
"linesColor": "#7f99cf",
},
"spacingUnit": 20,
@ -404,7 +407,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"weight": "300",
},
"code": Object {
"fontFamily": "\\"Lucida Console\\", Monaco, monospace",
"fontFamily": "Courirer, monospace",
"fontSize": "13px",
},
"colors": Object {
@ -446,6 +449,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"width": 40,
},
"schemaView": Object {
"defaultDetailsWidth": "75%",
"linesColor": "#7f99cf",
},
"spacingUnit": 20,
@ -508,7 +512,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"weight": "300",
},
"code": Object {
"fontFamily": "\\"Lucida Console\\", Monaco, monospace",
"fontFamily": "Courirer, monospace",
"fontSize": "13px",
},
"colors": Object {
@ -550,6 +554,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"width": 40,
},
"schemaView": Object {
"defaultDetailsWidth": "75%",
"linesColor": "#7f99cf",
},
"spacingUnit": 20,
@ -595,7 +600,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"weight": "300",
},
"code": Object {
"fontFamily": "\\"Lucida Console\\", Monaco, monospace",
"fontFamily": "Courirer, monospace",
"fontSize": "13px",
},
"colors": Object {
@ -637,6 +642,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"width": 40,
},
"schemaView": Object {
"defaultDetailsWidth": "75%",
"linesColor": "#7f99cf",
},
"spacingUnit": 20,
@ -706,7 +712,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"weight": "300",
},
"code": Object {
"fontFamily": "\\"Lucida Console\\", Monaco, monospace",
"fontFamily": "Courirer, monospace",
"fontSize": "13px",
},
"colors": Object {
@ -748,6 +754,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"width": 40,
},
"schemaView": Object {
"defaultDetailsWidth": "75%",
"linesColor": "#7f99cf",
},
"spacingUnit": 20,
@ -843,7 +850,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"weight": "300",
},
"code": Object {
"fontFamily": "\\"Lucida Console\\", Monaco, monospace",
"fontFamily": "Courirer, monospace",
"fontSize": "13px",
},
"colors": Object {
@ -885,6 +892,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"width": 40,
},
"schemaView": Object {
"defaultDetailsWidth": "75%",
"linesColor": "#7f99cf",
},
"spacingUnit": 20,
@ -947,7 +955,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"weight": "300",
},
"code": Object {
"fontFamily": "\\"Lucida Console\\", Monaco, monospace",
"fontFamily": "Courirer, monospace",
"fontSize": "13px",
},
"colors": Object {
@ -989,6 +997,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"width": 40,
},
"schemaView": Object {
"defaultDetailsWidth": "75%",
"linesColor": "#7f99cf",
},
"spacingUnit": 20,
@ -1034,7 +1043,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"weight": "300",
},
"code": Object {
"fontFamily": "\\"Lucida Console\\", Monaco, monospace",
"fontFamily": "Courirer, monospace",
"fontSize": "13px",
},
"colors": Object {
@ -1076,6 +1085,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView
"width": 40,
},
"schemaView": Object {
"defaultDetailsWidth": "75%",
"linesColor": "#7f99cf",
},
"spacingUnit": 20,
@ -1173,7 +1183,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"weight": "300",
},
"code": Object {
"fontFamily": "\\"Lucida Console\\", Monaco, monospace",
"fontFamily": "Courirer, monospace",
"fontSize": "13px",
},
"colors": Object {
@ -1215,6 +1225,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"width": 40,
},
"schemaView": Object {
"defaultDetailsWidth": "75%",
"linesColor": "#7f99cf",
},
"spacingUnit": 20,
@ -1284,7 +1295,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"weight": "300",
},
"code": Object {
"fontFamily": "\\"Lucida Console\\", Monaco, monospace",
"fontFamily": "Courirer, monospace",
"fontSize": "13px",
},
"colors": Object {
@ -1326,6 +1337,7 @@ exports[`Components SchemaView discriminator should correctly render discriminat
"width": 40,
},
"schemaView": Object {
"defaultDetailsWidth": "75%",
"linesColor": "#7f99cf",
},
"spacingUnit": 20,

View File

@ -1,16 +1,19 @@
import * as React from 'react';
import Stickyfill from 'stickyfill';
import { ComponentWithOptions } from '../OptionsProvider';
import { RedocNormalizedOptions, RedocRawOptions } from '../../services/RedocNormalizedOptions';
import styled from '../../styled-components';
let Stickyfill;
if (typeof window !== 'undefined') {
Stickyfill = require('stickyfill').default;
}
export interface StickySidebarProps {
className?: string;
scrollYOffset?: RedocRawOptions['scrollYOffset']; // passed directly or via context
}
const stickyfill = Stickyfill();
const stickyfill = Stickyfill && Stickyfill();
const StyledStickySidebar = styled.div`
width: ${props => props.theme.menu.width};
@ -31,11 +34,11 @@ export class StickySidebar extends ComponentWithOptions<StickySidebarProps> {
stickyElement: Element;
componentDidMount() {
stickyfill.add(this.stickyElement);
stickyfill && stickyfill.add(this.stickyElement);
}
componentWillUnmount() {
stickyfill.remove(this.stickyElement);
stickyfill && stickyfill.remove(this.stickyElement);
}
get scrollYOffset() {

View File

@ -1,4 +1,7 @@
const isSupported = document.queryCommandSupported && document.queryCommandSupported('copy');
const isSupported =
typeof document !== 'undefined' &&
document.queryCommandSupported &&
document.queryCommandSupported('copy');
export class ClipboardService {
static isSupported(): boolean {

View File

@ -1,5 +1,6 @@
import { bind, debounce } from 'decko';
import { EventEmitter } from 'eventemitter3';
import { isBrowser } from '../utils/';
const EVENT = 'hashchange';
@ -17,7 +18,7 @@ class _HistoryService {
}
get hash(): string {
return window.location.hash;
return isBrowser ? window.location.hash : '';
}
subscribe(cb): () => void {
@ -34,11 +35,15 @@ class _HistoryService {
};
bind() {
window.addEventListener('hashchange', this.emit, false);
if (isBrowser) {
window.addEventListener('hashchange', this.emit, false);
}
}
dispose() {
window.removeEventListener('hashchange', this.emit);
if (isBrowser) {
window.removeEventListener('hashchange', this.emit);
}
this.causedHashChange = false;
}
@ -47,11 +52,15 @@ class _HistoryService {
update(hash: string | null, rewriteHistory: boolean = false) {
if (hash == null || isSameHash(hash, this.hash)) return;
if (rewriteHistory) {
window.history.replaceState(null, '', window.location.href.split('#')[0] + '#' + hash);
if (isBrowser) {
window.history.replaceState(null, '', window.location.href.split('#')[0] + '#' + hash);
}
return;
}
this.causedHashChange = true;
window.location.hash = hash;
if (isBrowser) {
window.location.hash = hash;
}
}
}

View File

@ -3,7 +3,7 @@ import * as Remarkable from 'remarkable';
import { MDComponent } from '../components/Markdown/Markdown';
import { IMenuItem, SECTION_ATTR } from './MenuStore';
import { GroupModel } from './models';
import { highlight } from '../utils';
import { highlight, html2Str } from '../utils';
const md = new Remarkable('default', {
html: true,
@ -64,13 +64,6 @@ export class MarkdownRenderer {
attachHeadingsContent(rawText: string) {
const buildRegexp = heading => new RegExp(`<h\\d ${SECTION_ATTR}="section/${heading.id}">`);
const tmpEl = document.createElement('DIV');
const html2Str = html => {
tmpEl.innerHTML = html;
return tmpEl.innerText;
};
let flatHeadings = this.flattenHeadings(this.headings);
if (flatHeadings.length < 1) return;
let prevHeading = flatHeadings[0];

View File

@ -1,4 +1,5 @@
import { OperationModel, SpecStore } from './models';
import { querySelector } from '../utils/dom';
import { OperationModel, GroupModel, SpecStore } from './models';
import { computed, action } from 'mobx';
import { ScrollService } from './ScrollService';
@ -36,9 +37,9 @@ export class MenuStore {
/**
* cached flattened menu items to support absolute indexing
*/
private _flatItems: IMenuItem[];
private _unsubscribe: Function;
private _hashUnsubscribe: Function;
private _items?: (GroupModel | OperationModel)[];
/**
* active item absolute index (when flattened). -1 means nothing is selected
@ -60,7 +61,10 @@ export class MenuStore {
*/
@computed
get items(): IMenuItem[] {
return this.spec.operationGroups;
if (!this._items) {
this._items = this.spec.operationGroups;
}
return this._items;
}
/**
@ -139,7 +143,7 @@ export class MenuStore {
*/
getElementAt(idx: number): Element | null {
const item = this.flatItems[idx];
return (item && document.querySelector(`[${SECTION_ATTR}="${item.id}"]`)) || null;
return (item && querySelector(`[${SECTION_ATTR}="${item.id}"]`)) || null;
}
/**
@ -152,13 +156,11 @@ export class MenuStore {
/**
* flattened items as they appear in the tree depth-first (top to bottom in the view)
*/
@computed
get flatItems(): IMenuItem[] {
if (!this._flatItems) {
this._flatItems = flattenByProp(this.items, 'items');
this._flatItems.forEach((item, idx) => (item.absoluteIdx = idx));
}
return this._flatItems;
const flatItems = flattenByProp(this._items || [], 'items');
flatItems.forEach((item, idx) => (item.absoluteIdx = idx));
return flatItems;
}
/**
@ -188,7 +190,7 @@ export class MenuStore {
return;
}
this.activeItemIdx = item.absoluteIdx!;
this.activeItemIdx = item.absoluteIdx || -1;
if (updateHash) {
HistoryService.update(item.getHash(), rewriteHistory);
}

View File

@ -7,7 +7,7 @@ import { JsonPointer } from '../utils/JsonPointer';
import { isNamedDefinition } from '../utils/openapi';
import { COMPONENT_REGEXP, buildComponentComment } from './MarkdownRenderer';
import { RedocNormalizedOptions } from './RedocNormalizedOptions';
import { appendToMdHeading } from '../utils/';
import { appendToMdHeading, isBrowser } from '../utils/';
export type MergedOpenAPISchema = OpenAPISchema & { parentRefs?: string[] };
@ -52,10 +52,11 @@ export class OpenAPIParser {
this.spec = spec;
const href = isBrowser ? window.location.href : '';
if (typeof specUrl === 'string') {
this.specUrl = urlResolve(window.location.href, specUrl);
this.specUrl = urlResolve(href, specUrl);
} else {
this.specUrl = window.location.href;
this.specUrl = href;
}
}

View File

@ -1,5 +1,6 @@
import { ThemeInterface } from '../theme';
import { isNumeric, mergeObjects } from '../utils/helpers';
import { querySelector } from '../utils/dom';
import defaultTheme from '../theme';
export interface RedocRawOptions {
@ -69,7 +70,7 @@ export class RedocNormalizedOptions {
static normalizeScrollYOffset(value: RedocRawOptions['scrollYOffset']): () => number {
// just number is not valid selector and leads to crash so checking if isNumeric here
if (typeof value === 'string' && !isNumeric(value)) {
const el = document.querySelector(value);
const el = querySelector(value);
if (!el) {
console.warn(
'scrollYOffset value is a selector to non-existing element. Using offset 0 by default',

View File

@ -1,33 +1,35 @@
import { debounce, bind } from 'decko';
import { EventEmitter } from 'eventemitter3';
import { querySelector, isBrowser } from '../utils';
const EVENT = 'scroll';
export class ScrollService {
private _scrollParent: Window | HTMLElement;
private _scrollParent: Window | HTMLElement | undefined;
private _emiter: EventEmitter;
private _prevOffsetY: number = 0;
constructor() {
this._scrollParent = window;
this._scrollParent = isBrowser ? window : undefined;
this._emiter = new EventEmitter();
this.bind();
}
bind() {
this._prevOffsetY = this.scrollY();
this._scrollParent.addEventListener('scroll', this.handleScroll);
this._scrollParent && this._scrollParent.addEventListener('scroll', this.handleScroll);
}
dispose() {
this._scrollParent.removeEventListener('scroll', this.handleScroll);
this._scrollParent && this._scrollParent.removeEventListener('scroll', this.handleScroll);
this._emiter.removeAllListeners(EVENT);
}
scrollY(): number {
if (this._scrollParent === window) {
return window.pageYOffset;
} else if (this._scrollParent instanceof HTMLElement) {
if (typeof HTMLElement !== 'undefined' && this._scrollParent instanceof HTMLElement) {
return this._scrollParent.scrollTop;
} else if (this._scrollParent !== undefined) {
return (this._scrollParent as Window).pageYOffset;
} else {
return 0;
}
@ -56,7 +58,7 @@ export class ScrollService {
}
scrollIntoViewBySelector(selector: string) {
const element = document.querySelector(selector);
const element = querySelector(selector);
this.scrollIntoView(element);
}

View File

@ -1,5 +1,6 @@
import { OpenAPIContact, OpenAPIInfo, OpenAPILicense } from '../../types';
import { OpenAPIParser } from '../OpenAPIParser';
import { isBrowser } from '../../utils/';
export class ApiInfoModel implements OpenAPIInfo {
title: string;
@ -15,7 +16,7 @@ export class ApiInfoModel implements OpenAPIInfo {
}
get downloadLink() {
if (!this.parser.specUrl && window.Blob && window.URL) {
if (!this.parser.specUrl && isBrowser && window.Blob && window.URL) {
const blob = new Blob([JSON.stringify(this.parser.spec, null, 2)], {
type: 'application/json',
});
@ -25,7 +26,7 @@ export class ApiInfoModel implements OpenAPIInfo {
}
get downloadFileName(): string | undefined {
if (!this.parser.specUrl && window.Blob && window.URL) {
if (!this.parser.specUrl && isBrowser && window.Blob && window.URL) {
return 'swagger.json';
}
return undefined;

View File

@ -1,6 +1,7 @@
import { render } from 'react-dom';
import * as React from 'react';
import { querySelector } from './utils/dom';
import { RedocStandalone } from './components/RedocStandalone';
export const version = __REDOC_VERSION__;
@ -30,7 +31,7 @@ function parseOptionsFromElement(element: Element) {
export function init(
specOrSpecUrl: string | any,
options: any = {},
element: Element | null = document.querySelector('redoc'),
element: Element | null = querySelector('redoc'),
) {
if (element === null) {
throw new Error('"element" argument is not provided and <redoc> tag is not found on the page');
@ -63,7 +64,7 @@ export function init(
* autoinit ReDoc if <redoc> tag is found on the page with "spec-url" attr
*/
function autoInit() {
const element = document.querySelector('redoc');
const element = querySelector('redoc');
if (!element) {
return;
}

25
src/utils/dom.ts Normal file
View File

@ -0,0 +1,25 @@
export const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';
export function querySelector(selector: string): Element | null {
if (typeof document !== 'undefined') {
return document.querySelector(selector);
}
return null;
}
/**
* Drop everything inside <...> (i.e., tags/elements), and keep the text.
* Unlike browser innerText, this removes newlines; it also doesn't handle
* un-encoded `<` or `>` characters very well, so don't feed it malformed HTML
*/
export function html2Str(html: string): string {
return html
.split(/<[^>]+>/)
.map(function(chunk) {
return chunk.trim();
})
.filter(function(trimmedChunk) {
return trimmedChunk.length > 0;
})
.join(' ');
}

View File

@ -20,7 +20,10 @@ import 'prismjs/components/prism-objectivec.js';
import 'prismjs/components/prism-scala.js';
import 'prismjs/components/prism-markup.js'; // xml
import 'prismjs/themes/prism-dark.css'; // dark theme
import { injectGlobal } from '../styled-components';
import prismStyles from 'prismjs/themes/prism-dark.css'; // dark theme
injectGlobal`${prismStyles}`;
const DEFAULT_LANG = 'clike';

View File

@ -5,3 +5,4 @@ export * from './openapi';
export * from './helpers';
export * from './highlight';
export * from './loadAndBundleSpec';
export * from './dom';

View File

@ -92,16 +92,13 @@ export default env => {
},
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
sourceMap: true,
minimize: true,
},
use: {
loader: 'css-loader',
options: {
sourceMap: true,
minimize: true,
},
],
},
},
{ enforce: 'pre', test: /\.js$/, loader: 'source-map-loader' },
],
@ -128,7 +125,7 @@ export default env => {
if (!env.standalone) {
config.externals = (context, request, callback) => {
// ignore node-fetch dep of swagger2openapi as it is not used
if (/node-fetch$/i.test(request)) return callback(null, 'var fetch');
if (/node-fetch$/i.test(request)) return callback(null, 'var undefined');
return nodeExternals(context, request, callback);
};
}

View File

@ -424,7 +424,7 @@ async@^1.4.0, async@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
async@^2.1.2, async@^2.1.4, async@^2.4.1, async@^2.5.0:
async@^2.1.2, async@^2.1.4, async@^2.5.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4"
dependencies:
@ -2505,15 +2505,6 @@ extglob@^2.0.2:
snapdragon "^0.8.1"
to-regex "^3.0.1"
extract-text-webpack-plugin@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-3.0.2.tgz#5f043eaa02f9750a9258b78c0a6e0dc1408fb2f7"
dependencies:
async "^2.4.1"
loader-utils "^1.1.0"
schema-utils "^0.3.0"
webpack-sources "^1.0.1"
extract-zip@1.6.5:
version "1.6.5"
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.5.tgz#99a06735b6ea20ea9b705d779acffcc87cff0440"