inside
return ;
}
return ;
};
Pre.propTypes = {
children: PropTypes.node,
};
export const baseOverrides = {
a: {
component: Link as React.FC,
},
h1: {
component: MarkdownHeading as React.FC,
props: {
level: 1,
},
},
h2: {
component: MarkdownHeading as React.FC,
props: {
level: 2,
},
},
h3: {
component: MarkdownHeading as React.FC,
props: {
level: 3,
},
},
h4: {
component: MarkdownHeading as React.FC,
props: {
level: 4,
},
},
h5: {
component: MarkdownHeading as React.FC,
props: {
level: 5,
},
},
h6: {
component: MarkdownHeading as React.FC,
props: {
level: 6,
},
},
p: {
component: Para as React.FC,
props: {
semantic: 'p',
},
},
em: {
component: Text as React.FC,
props: {
semantic: 'em',
},
},
strong: {
component: Text as React.FC,
props: {
semantic: 'strong',
},
},
ul: {
component: List as React.FC,
},
ol: {
component: List as React.FC,
props: {
ordered: true,
},
},
blockquote: {
component: Blockquote as React.FC,
},
code: {
component: Code as React.FC,
},
pre: {
component: Pre as React.FC,
},
input: {
component: Checkbox as React.FC,
},
hr: {
component: Hr as React.FC,
},
table: {
component: Table as React.FC,
},
thead: {
component: TableHead as React.FC,
},
th: {
component: TableCell as React.FC,
props: {
header: true,
},
},
tbody: {
component: TableBody as React.FC,
},
tr: {
component: TableRow as React.FC,
},
td: {
component: TableCell as React.FC,
},
details: {
component: Details as React.FC,
},
summary: {
component: DetailsSummary as React.FC,
},
};
export const inlineOverrides = {
...baseOverrides,
p: {
component: Text,
},
};
interface MarkdownProps {
text: string;
inline?: boolean;
}
export const Markdown: React.FunctionComponent = ({ text, inline }) => {
const overrides = inline ? inlineOverrides : baseOverrides;
return compiler(stripHtmlComments(text), { overrides, forceBlock: true });
};
Markdown.propTypes = {
text: PropTypes.string.isRequired,
inline: PropTypes.bool,
};
export default Markdown;
================================================
FILE: src/client/rsg-components/Markdown/MarkdownHeading/MarkdownHeading.spec.tsx
================================================
import React from 'react';
import renderer from 'react-test-renderer';
import MarkdownHeading from './index';
describe('Markdown Heading', () => {
it('should render a heading with a wrapper that provides margin and an id', () => {
const actual = renderer.create(
The markdown heading
);
expect(actual).toMatchSnapshot();
});
});
================================================
FILE: src/client/rsg-components/Markdown/MarkdownHeading/MarkdownHeadingRenderer.tsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import Styled, { JssInjectedProps } from 'rsg-components/Styled';
import Heading from 'rsg-components/Heading';
import * as Rsg from '../../../../typings';
const styles = ({ space }: Rsg.Theme) => ({
spacing: {
marginBottom: space[2],
},
});
interface MarkdownHeadingProps extends JssInjectedProps {
children: React.ReactNode;
level: number;
id?: string;
}
const MarkdownHeadingRenderer: React.FunctionComponent = ({
classes,
level,
children,
id,
}) => {
return (
{children}
);
};
MarkdownHeadingRenderer.propTypes = {
classes: PropTypes.objectOf(PropTypes.string.isRequired).isRequired,
level: PropTypes.oneOf([1, 2, 3, 4, 5, 6]).isRequired,
children: PropTypes.any,
id: PropTypes.string,
};
export default Styled(styles)(MarkdownHeadingRenderer);
================================================
FILE: src/client/rsg-components/Markdown/MarkdownHeading/__snapshots__/MarkdownHeading.spec.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Markdown Heading should render a heading with a wrapper that provides margin and an id 1`] = `
The markdown heading
`;
================================================
FILE: src/client/rsg-components/Markdown/MarkdownHeading/index.ts
================================================
export { default } from 'rsg-components/Markdown/MarkdownHeading/MarkdownHeadingRenderer';
================================================
FILE: src/client/rsg-components/Markdown/Pre/Pre.spec.tsx
================================================
import React from 'react';
import renderer from 'react-test-renderer';
import Pre from './index';
describe('Markdown Pre', () => {
it('should render a pre', () => {
const actual = renderer.create(This is pre-formatted text. );
expect(actual.toJSON()).toMatchSnapshot();
});
it('should render highlighted code', () => {
const code = 'OK ';
const actual = renderer.create({code} );
expect(actual.toJSON()).toMatchSnapshot();
});
});
================================================
FILE: src/client/rsg-components/Markdown/Pre/PreRenderer.tsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'clsx';
import Styled, { JssInjectedProps } from 'rsg-components/Styled';
import prismTheme from '../../../styles/prismTheme';
import * as Rsg from '../../../../typings';
const styles = ({ space, color, fontSize, fontFamily, borderRadius }: Rsg.Theme) => ({
pre: {
fontFamily: fontFamily.monospace,
fontSize: fontSize.small,
lineHeight: 1.5,
color: color.base,
whiteSpace: 'pre-wrap',
wordWrap: 'normal',
tabSize: 2,
hyphens: 'none',
backgroundColor: color.codeBackground,
padding: [[space[1], space[2]]],
border: [[1, color.codeBackground, 'solid']],
borderRadius,
marginTop: 0,
marginBottom: space[2],
overflow: 'auto',
...prismTheme({ color }),
},
});
export interface PreProps {
className?: string;
children: React.ReactNode;
}
type PrePropsWithClasses = JssInjectedProps & PreProps;
export const PreRenderer: React.FunctionComponent = ({
classes,
className,
children,
}) => {
const classNames = cx(className, classes.pre);
const isHighlighted = className && className.indexOf('lang-') !== -1;
if (isHighlighted && children) {
return ;
}
return {children} ;
};
PreRenderer.propTypes = {
classes: PropTypes.objectOf(PropTypes.string.isRequired).isRequired,
className: PropTypes.string,
children: PropTypes.any.isRequired,
};
export default Styled(styles)(PreRenderer);
================================================
FILE: src/client/rsg-components/Markdown/Pre/__snapshots__/Pre.spec.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Markdown Pre should render a pre 1`] = `
This is pre-formatted text.
`;
exports[`Markdown Pre should render highlighted code 1`] = `
OK",
}
}
/>
`;
================================================
FILE: src/client/rsg-components/Markdown/Pre/index.ts
================================================
export { default } from 'rsg-components/Markdown/Pre/PreRenderer';
export * from 'rsg-components/Markdown/Pre/PreRenderer';
================================================
FILE: src/client/rsg-components/Markdown/Table/Table.spec.tsx
================================================
import React from 'react';
import renderer from 'react-test-renderer';
import { Table, TableHead, TableBody, TableRow, TableCell } from './index';
describe('Markdown Table', () => {
it('should render a table', () => {
const actual = renderer.create(
1st header
2nd header
1st cell
2nd cell
);
expect(actual.toJSON()).toMatchSnapshot();
});
});
================================================
FILE: src/client/rsg-components/Markdown/Table/TableBodyRenderer.tsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
interface Props {
children?: React.ReactNode;
}
export const TableBodyRenderer = ({ children }: Props) => {
return {children} ;
};
TableBodyRenderer.propTypes = {
children: PropTypes.node.isRequired,
};
export default TableBodyRenderer;
================================================
FILE: src/client/rsg-components/Markdown/Table/TableCellRenderer.tsx
================================================
import React from 'react';
import Styled, { JssInjectedProps } from 'rsg-components/Styled';
import * as Rsg from '../../../../typings';
const styles = ({ space, color, fontSize, fontFamily }: Rsg.Theme) => ({
td: {
padding: [[space[0], space[2], space[0], 0]],
fontFamily: fontFamily.base,
fontSize: fontSize.base,
color: color.base,
lineHeight: 1.5,
},
th: {
composes: '$td',
fontWeight: 'bold',
},
});
interface TableCellProps extends JssInjectedProps {
children: React.ReactNode;
header?: boolean;
}
export const TableCellRenderer: React.FunctionComponent = ({
classes,
header = false,
children,
}) => {
if (header) {
return {children} ;
}
return {children} ;
};
export default Styled(styles)(TableCellRenderer);
================================================
FILE: src/client/rsg-components/Markdown/Table/TableHeadRenderer.tsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import Styled, { JssInjectedProps } from 'rsg-components/Styled';
import * as Rsg from '../../../../typings';
const styles = ({ color }: Rsg.Theme) => ({
thead: {
borderBottom: [[1, color.border, 'solid']],
},
});
interface TableHeadProps extends JssInjectedProps {
children: React.ReactNode;
}
export const TableHeadRenderer: React.FunctionComponent = ({
classes,
children,
}) => {
return {children} ;
};
TableHeadRenderer.propTypes = {
classes: PropTypes.objectOf(PropTypes.string.isRequired).isRequired,
children: PropTypes.any.isRequired,
};
export default Styled(styles)(TableHeadRenderer);
================================================
FILE: src/client/rsg-components/Markdown/Table/TableRenderer.tsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import Styled, { JssInjectedProps } from 'rsg-components/Styled';
import * as Rsg from '../../../../typings';
const styles = ({ space }: Rsg.Theme) => ({
table: {
marginTop: 0,
marginBottom: space[2],
borderCollapse: 'collapse',
},
});
interface TableProps extends JssInjectedProps {
children: React.ReactNode;
}
export const TableRenderer: React.FunctionComponent = ({ classes, children }) => {
return ;
};
TableRenderer.propTypes = {
classes: PropTypes.objectOf(PropTypes.string.isRequired).isRequired,
children: PropTypes.any.isRequired,
};
export default Styled(styles)(TableRenderer);
================================================
FILE: src/client/rsg-components/Markdown/Table/TableRowRenderer.tsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
interface Props {
children?: React.ReactNode;
}
export const TableRowRenderer = ({ children }: Props) => {
return {children} ;
};
TableRowRenderer.propTypes = {
children: PropTypes.node.isRequired,
};
export default TableRowRenderer;
================================================
FILE: src/client/rsg-components/Markdown/Table/__snapshots__/Table.spec.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Markdown Table should render a table 1`] = `
1st header
2nd header
1st cell
2nd cell
`;
================================================
FILE: src/client/rsg-components/Markdown/Table/index.ts
================================================
export { default as Table } from 'rsg-components/Markdown/Table/TableRenderer';
export { default as TableHead } from 'rsg-components/Markdown/Table/TableHeadRenderer';
export { default as TableBody } from 'rsg-components/Markdown/Table/TableBodyRenderer';
export { default as TableRow } from 'rsg-components/Markdown/Table/TableRowRenderer';
export { default as TableCell } from 'rsg-components/Markdown/Table/TableCellRenderer';
================================================
FILE: src/client/rsg-components/Markdown/__snapshots__/Markdown.spec.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Markdown inline should render text in a span 1`] = `
Hello world!
`;
exports[`Markdown should render a blockquote 1`] = `
This is a blockquote.
And this is a second line.
`;
exports[`Markdown should render a horizontal rule 1`] = ` `;
exports[`Markdown should render a table 1`] = `
heading 1
heading 2
foo
bar
more foo
more bar
`;
exports[`Markdown should render check-lists 1`] = `
`;
exports[`Markdown should render code blocks without escaping 1`] = `
`;
exports[`Markdown should render emphasis and strong text 1`] = `
this text is
strong
and this is
emphasized
`;
exports[`Markdown should render headings with generated ids 1`] = `
one
two
three
four
five
six
`;
exports[`Markdown should render inline code with escaping 1`] = `
Foo
<bar>
baz
`;
exports[`Markdown should render links 1`] = `
a
link
`;
exports[`Markdown should render mixed nested lists 1`] = `
list 1
list 2
Sub-list
Sub-list
Sub-list
list 3
`;
exports[`Markdown should render ordered lists 1`] = `
list
item
three
`;
exports[`Markdown should render paragraphs 1`] = `
a paragraph
another paragraph
`;
exports[`Markdown should render pre-formatted text 1`] = `
this is preformatted
so is this
`;
exports[`Markdown should render unordered lists 1`] = `
`;
================================================
FILE: src/client/rsg-components/Markdown/index.ts
================================================
export { default } from 'rsg-components/Markdown/Markdown';
================================================
FILE: src/client/rsg-components/Message/Message.spec.tsx
================================================
import React from 'react';
import { createRenderer } from 'react-test-renderer/shallow';
import { MessageRenderer } from './MessageRenderer';
it('renderer should render message', () => {
const message = 'Hello *world*!';
const renderer = createRenderer();
renderer.render({message} );
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
it('renderer should render message for array', () => {
const messages = ['Hello *world*!', 'Foo _bar_'];
const renderer = createRenderer();
renderer.render({messages} );
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
================================================
FILE: src/client/rsg-components/Message/MessageRenderer.tsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import Markdown from 'rsg-components/Markdown';
import Styled, { JssInjectedProps } from 'rsg-components/Styled';
import * as Rsg from '../../../typings';
const styles = ({ space }: Rsg.Theme) => ({
root: {
marginBottom: space[4],
},
});
interface MessageProps extends JssInjectedProps {
children: React.ReactNode;
}
export const MessageRenderer: React.FunctionComponent = ({ classes, children }) => {
return (
);
};
MessageRenderer.propTypes = {
classes: PropTypes.objectOf(PropTypes.string.isRequired).isRequired,
children: PropTypes.any.isRequired,
};
export default Styled(styles)(MessageRenderer);
================================================
FILE: src/client/rsg-components/Message/__snapshots__/Message.spec.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renderer should render message 1`] = `
`;
exports[`renderer should render message for array 1`] = `
`;
================================================
FILE: src/client/rsg-components/Message/index.ts
================================================
export { default } from 'rsg-components/Message/MessageRenderer';
================================================
FILE: src/client/rsg-components/Methods/Methods.spec.tsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import { parse, MethodDescriptor } from 'react-docgen';
import { createRenderer } from 'react-test-renderer/shallow';
import MethodsRenderer, { columns } from './MethodsRenderer';
// Test renderers with clean readable snapshot diffs
export default function ColumnsRenderer({ methods }: { methods: MethodDescriptor[] }) {
return (
{methods.map((row, rowIdx) => (
{columns.map((col, colIdx) => (
{col.render(row)}
))}
))}
);
}
ColumnsRenderer.propTypes = {
methods: PropTypes.array,
};
function render(methods: string[]) {
const parsed = parse(
`
import { Component } from 'react';
export default class Cmpnt extends Component {
${methods.join('\n')}
render() {
}
}
`,
undefined,
undefined,
{ filename: '' }
);
const renderer = createRenderer();
if (Array.isArray(parsed) || !parsed.methods) {
renderer.render(
);
} else {
renderer.render( );
}
return renderer.getRenderOutput();
}
describe('MethodsRenderer', () => {
it('should render a table', () => {
const renderer = createRenderer();
renderer.render(
);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
});
describe('PropsRenderer', () => {
it('should render public method', () => {
const actual = render(['/**\n * Public\n * @public\n */\nmethod() {}']);
expect(actual).toMatchSnapshot();
});
it('should render parameters', () => {
const actual = render([
'/**\n * Public\n * @public\n * @param {Number} value - Description\n */\nmethod(value) {}',
]);
expect(actual).toMatchSnapshot();
});
it('should render returns', () => {
const actual = render([
'/**\n * @public\n * @returns {Number} - Description\n */\nmethod() {}',
]);
expect(actual).toMatchSnapshot();
});
it('should render JsDoc tags', () => {
const renderer = createRenderer();
renderer.render(
);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
it('should render deprecated JsDoc tags', () => {
const renderer = createRenderer();
renderer.render(
);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
});
================================================
FILE: src/client/rsg-components/Methods/MethodsRenderer.tsx
================================================
import React from 'react';
import Markdown from 'rsg-components/Markdown';
import Argument from 'rsg-components/Argument';
import Arguments from 'rsg-components/Arguments';
import Name from 'rsg-components/Name';
import JsDoc from 'rsg-components/JsDoc';
import Table from 'rsg-components/Table';
import { MethodDescriptor } from 'react-docgen';
const getRowKey = (row: MethodDescriptor): string => row.name;
export const columns = [
{
caption: 'Method name',
// eslint-disable-next-line react/prop-types
render: ({ name, tags = {} }: MethodDescriptor) => (
{`${name}()`}
),
},
{
caption: 'Parameters',
// eslint-disable-next-line react/prop-types
render: ({ params = [] }: MethodDescriptor) => ,
},
{
caption: 'Description',
// eslint-disable-next-line react/prop-types
render: ({ description, returns, tags = {} }: MethodDescriptor) => (
{description &&
}
{returns &&
}
),
},
];
const MethodsRenderer: React.FunctionComponent<{ methods: MethodDescriptor[] }> = ({ methods }) => (
);
export default MethodsRenderer;
================================================
FILE: src/client/rsg-components/Methods/__snapshots__/Methods.spec.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MethodsRenderer should render a table 1`] = `
`;
exports[`PropsRenderer should render JsDoc tags 1`] = `
`;
exports[`PropsRenderer should render deprecated JsDoc tags 1`] = `
`;
exports[`PropsRenderer should render parameters 1`] = `
`;
exports[`PropsRenderer should render public method 1`] = `
`;
exports[`PropsRenderer should render returns 1`] = `
`;
================================================
FILE: src/client/rsg-components/Methods/index.ts
================================================
export { default } from 'rsg-components/Methods/MethodsRenderer';
export * from 'rsg-components/Methods/MethodsRenderer';
================================================
FILE: src/client/rsg-components/Name/Name.spec.tsx
================================================
import React from 'react';
import { createRenderer } from 'react-test-renderer/shallow';
import { NameRenderer, styles } from './NameRenderer';
const props = {
classes: classes(styles),
};
it('renderer should render argument name', () => {
const renderer = createRenderer();
renderer.render(Foo );
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
it('renderer should render deprecated argument name', () => {
const renderer = createRenderer();
renderer.render(
Foo
);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
================================================
FILE: src/client/rsg-components/Name/NameRenderer.tsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'clsx';
import Styled, { JssInjectedProps } from 'rsg-components/Styled';
import * as Rsg from '../../../typings';
export const styles = ({ fontFamily, fontSize, color }: Rsg.Theme) => ({
name: {
fontFamily: fontFamily.monospace,
fontSize: fontSize.small,
color: color.name,
},
isDeprecated: {
color: color.light,
textDecoration: 'line-through',
},
});
interface NameProps extends JssInjectedProps {
children: React.ReactNode;
deprecated?: boolean;
}
export const NameRenderer: React.FunctionComponent = ({
classes,
children,
deprecated,
}) => {
const classNames = cx(classes.name, {
[classes.isDeprecated]: deprecated,
});
return {children};
};
NameRenderer.propTypes = {
classes: PropTypes.objectOf(PropTypes.string.isRequired).isRequired,
children: PropTypes.any.isRequired,
deprecated: PropTypes.bool,
};
export default Styled(styles)(NameRenderer);
================================================
FILE: src/client/rsg-components/Name/__snapshots__/Name.spec.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renderer should render argument name 1`] = `
Foo
`;
exports[`renderer should render deprecated argument name 1`] = `
Foo
`;
================================================
FILE: src/client/rsg-components/Name/index.ts
================================================
export { default } from 'rsg-components/Name/NameRenderer';
================================================
FILE: src/client/rsg-components/NotFound/NotFound.spec.tsx
================================================
import React from 'react';
import { createRenderer } from 'react-test-renderer/shallow';
import { NotFoundRenderer } from './NotFoundRenderer';
it('renderer should render not found message', () => {
const renderer = createRenderer();
renderer.render( );
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
================================================
FILE: src/client/rsg-components/NotFound/NotFoundRenderer.tsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import Markdown from 'rsg-components/Markdown';
import Styled, { JssInjectedProps } from 'rsg-components/Styled';
import * as Rsg from '../../../typings';
const styles = ({ maxWidth }: Rsg.Theme) => ({
root: {
maxWidth,
margin: [[0, 'auto']],
},
});
export const NotFoundRenderer: React.FunctionComponent = ({ classes }) => {
return (
);
};
NotFoundRenderer.propTypes = {
classes: PropTypes.objectOf(PropTypes.string.isRequired).isRequired,
};
export default Styled(styles)(NotFoundRenderer);
================================================
FILE: src/client/rsg-components/NotFound/__snapshots__/NotFound.spec.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renderer should render not found message 1`] = `
`;
================================================
FILE: src/client/rsg-components/NotFound/index.ts
================================================
export { default } from 'rsg-components/NotFound/NotFoundRenderer';
================================================
FILE: src/client/rsg-components/Para/Para.spec.tsx
================================================
import React from 'react';
import { createRenderer } from 'react-test-renderer/shallow';
import { ParaRenderer, styles } from './ParaRenderer';
const props = {
classes: classes(styles),
};
it('should render paragraph as a ', () => {
const renderer = createRenderer();
renderer.render(
Pizza );
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
it('should render paragraph as a
', () => {
const renderer = createRenderer();
renderer.render(
Pizza
);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
================================================
FILE: src/client/rsg-components/Para/ParaRenderer.tsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import Styled, { JssInjectedProps } from 'rsg-components/Styled';
import * as Rsg from '../../../typings';
export const styles = ({ space, color, fontFamily, fontSize }: Rsg.Theme) => ({
para: {
marginTop: 0,
marginBottom: space[2],
color: color.base,
fontFamily: fontFamily.base,
fontSize: fontSize.text,
lineHeight: 1.5,
},
});
interface ParaProps extends JssInjectedProps {
semantic?: 'p';
children: React.ReactNode;
}
export const ParaRenderer: React.FunctionComponent = ({
classes,
semantic,
children,
}) => {
const Tag = semantic || 'div';
return {children} ;
};
ParaRenderer.propTypes = {
classes: PropTypes.objectOf(PropTypes.string.isRequired).isRequired,
semantic: PropTypes.oneOf(['p']),
children: PropTypes.any.isRequired,
};
export default Styled(styles)(ParaRenderer);
================================================
FILE: src/client/rsg-components/Para/__snapshots__/Para.spec.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render paragraph as a 1`] = `
Pizza
`;
exports[`should render paragraph as a
1`] = `
Pizza
`;
================================================
FILE: src/client/rsg-components/Para/index.ts
================================================
export { default } from 'rsg-components/Para/ParaRenderer';
================================================
FILE: src/client/rsg-components/Pathline/Pathline.spec.tsx
================================================
import { fireEvent, render } from '@testing-library/react';
import React from 'react';
import copy from 'clipboard-copy';
import { createRenderer } from 'react-test-renderer/shallow';
import { PathlineRenderer, styles } from './PathlineRenderer';
jest.mock('clipboard-copy');
const pathline = 'foo/bar';
const props = {
classes: classes(styles),
};
it('renderer should a path line', () => {
const renderer = createRenderer();
renderer.render(
{pathline} );
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
test('should copy text on click', () => {
const { getByRole } = render(
{pathline} );
fireEvent.click(getByRole('button'));
expect(copy).toBeCalledWith(pathline);
});
================================================
FILE: src/client/rsg-components/Pathline/PathlineRenderer.tsx
================================================
import React from 'react';
import copy from 'clipboard-copy';
import { MdContentCopy } from 'react-icons/md';
import ToolbarButton from 'rsg-components/ToolbarButton';
import Styled, { JssInjectedProps } from 'rsg-components/Styled';
import * as Rsg from '../../../typings';
export const styles = ({ space, fontFamily, fontSize, color }: Rsg.Theme) => ({
pathline: {
fontFamily: fontFamily.monospace,
fontSize: fontSize.small,
color: color.light,
wordBreak: 'break-all',
},
copyButton: {
marginLeft: space[0],
},
});
interface Props extends JssInjectedProps {
children?: React.ReactNode;
}
export const PathlineRenderer = ({ classes, children }: Props) => {
return (
{children}
children && copy(children.toString())}
title="Copy to clipboard"
>
);
};
export default Styled
(styles)(PathlineRenderer);
================================================
FILE: src/client/rsg-components/Pathline/__snapshots__/Pathline.spec.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renderer should a path line 1`] = `
foo/bar
`;
================================================
FILE: src/client/rsg-components/Pathline/index.ts
================================================
export { default } from 'rsg-components/Pathline/PathlineRenderer';
================================================
FILE: src/client/rsg-components/Playground/Playground.spec.tsx
================================================
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import Playground from './Playground';
import slots from '../slots';
import Context from '../Context';
const evalInContext = (a: string) =>
// eslint-disable-next-line no-new-func
new Function('require', 'const React = require("react");' + a).bind(null, require);
const code = 'Code: OK ';
const newCode = 'Code: Not OK ';
const defaultProps = {
index: 0,
name: 'name',
settings: {},
exampleMode: 'collapse',
evalInContext,
code,
};
const context = {
config: {
previewDelay: 0,
},
codeRevision: 0,
slots: slots(),
};
const Provider = (props: any) => ;
it('should update code via props', () => {
const { rerender, getByText } = render(
);
expect(getByText('Code: OK')).toBeInTheDocument();
rerender(
);
expect(getByText('Code: Not OK')).toBeInTheDocument();
});
it('should open a code editor', () => {
const { queryByRole, getByText } = render(
);
expect(queryByRole('textbox')).not.toBeInTheDocument();
fireEvent.click(getByText(/view code/i));
expect(queryByRole('textbox')).toBeInTheDocument();
});
it('should not render a code editor if noeditor option passed in example settings', () => {
const { queryByText } = render(
);
expect(queryByText(/view code/i)).not.toBeInTheDocument();
});
it('should open a code editor by default if showcode=true option passed in example settings', () => {
const { queryByRole } = render(
);
expect(queryByRole('textbox')).toBeInTheDocument();
});
it('should open a code editor by default if exampleMode="expand" option specified in style guide config', () => {
const { queryByRole } = render(
);
expect(queryByRole('textbox')).toBeInTheDocument();
});
it('showcode option in example settings should overwrite style guide config option', () => {
const { queryByRole } = render(
);
expect(queryByRole('textbox')).not.toBeInTheDocument();
});
it('should not include padded class if padded option is not passed in example settings', () => {
const { getByTestId } = render(
);
expect(getByTestId('preview-wrapper')).not.toHaveAttribute(
'class',
expect.stringContaining('rsg--padded-')
);
});
it('should include padded class if padded option is passed in example settings', () => {
const { getByTestId } = render(
);
expect(getByTestId('preview-wrapper')).toHaveAttribute(
'class',
expect.stringContaining('rsg--padded-')
);
});
================================================
FILE: src/client/rsg-components/Playground/Playground.tsx
================================================
import React, { Component } from 'react';
import debounce from 'lodash/debounce';
import Preview from 'rsg-components/Preview';
import Para from 'rsg-components/Para';
import Slot from 'rsg-components/Slot';
import PlaygroundRenderer from 'rsg-components/Playground/PlaygroundRenderer';
import Context, { StyleGuideContextContents } from 'rsg-components/Context';
import { EXAMPLE_TAB_CODE_EDITOR } from '../slots';
import { DisplayModes, ExampleModes } from '../../consts';
interface PlaygroundProps {
evalInContext(code: string): () => any;
index: number;
name?: string;
exampleMode?: string;
code: string;
settings: {
showcode?: boolean;
noeditor?: boolean;
padded?: boolean;
// TODO: better typing for this
props?: any;
};
}
interface PlaygroundState {
code: string;
prevCode: string;
activeTab?: string;
}
class Playground extends Component {
public static contextType = Context;
private handleChange = debounce((code) => {
this.setState({
code,
});
}, (this.context as StyleGuideContextContents).config.previewDelay);
public state: PlaygroundState = {
code: this.props.code,
prevCode: this.props.code,
activeTab: this.getInitialActiveTab() ? EXAMPLE_TAB_CODE_EDITOR : undefined,
};
public static getDerivedStateFromProps(nextProps: PlaygroundProps, prevState: PlaygroundState) {
const { code } = nextProps;
if (prevState.prevCode !== code) {
return {
prevCode: code,
code,
};
}
return null;
}
public componentWillUnmount() {
// Clear pending changes
this.handleChange.cancel();
}
private getInitialActiveTab(): boolean {
const expandCode = this.props.exampleMode === ExampleModes.expand;
return this.props.settings.showcode !== undefined ? this.props.settings.showcode : expandCode;
}
private handleTabChange = (name: string) => {
this.setState((state) => ({
activeTab: state.activeTab !== name ? name : undefined,
}));
};
public render() {
const { code, activeTab } = this.state;
const { evalInContext, index, name, settings, exampleMode } = this.props;
const { displayMode } = this.context as StyleGuideContextContents;
const isExampleHidden = exampleMode === ExampleModes.hide;
const isEditorHidden = settings.noeditor || isExampleHidden;
const preview = ;
return isEditorHidden ? (
{preview}
) : (
}
tabBody={
}
toolbar={
}
/>
);
}
}
export default Playground;
================================================
FILE: src/client/rsg-components/Playground/PlaygroundRenderer.tsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'clsx';
import Styled, { JssInjectedProps } from 'rsg-components/Styled';
import * as Rsg from '../../../typings';
export const styles = ({ space, color, borderRadius }: Rsg.Theme) => ({
root: {
marginBottom: space[4],
},
preview: {
padding: space[2],
border: [[1, color.border, 'solid']],
borderRadius,
// the next 2 lines are required to contain floated components
width: '100%',
display: 'inline-block',
},
controls: {
display: 'flex',
alignItems: 'center',
marginBottom: space[1],
},
toolbar: {
marginLeft: 'auto',
},
tab: {}, // expose className to allow using it in 'styles' settings
padded: {
// add padding between each example element rendered
'& > *': {
isolate: false,
marginLeft: -space[1],
marginRight: -space[1],
'& > *': {
isolate: false,
marginRight: space[1],
marginLeft: space[1],
},
},
},
});
interface PlaygroundRendererProps extends JssInjectedProps {
exampleIndex: number;
name?: string;
padded: boolean;
preview: React.ReactNode;
// TODO: need to find a better type here too
previewProps: any;
tabButtons: React.ReactNode;
tabBody: React.ReactNode;
toolbar: React.ReactNode;
}
export const PlaygroundRenderer: React.FunctionComponent = ({
classes,
exampleIndex,
name,
padded,
preview,
previewProps,
tabButtons,
tabBody,
toolbar,
}) => {
const { className, ...props } = previewProps;
const previewClasses = cx(classes.preview, className, { [classes.padded]: padded });
return (
);
};
PlaygroundRenderer.propTypes = {
classes: PropTypes.objectOf(PropTypes.string.isRequired).isRequired,
exampleIndex: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
padded: PropTypes.bool.isRequired,
preview: PropTypes.any.isRequired,
previewProps: PropTypes.object.isRequired,
tabButtons: PropTypes.any.isRequired,
tabBody: PropTypes.any.isRequired,
toolbar: PropTypes.any.isRequired,
};
export default Styled(styles)(PlaygroundRenderer);
================================================
FILE: src/client/rsg-components/Playground/index.ts
================================================
export { default } from 'rsg-components/Playground/Playground';
================================================
FILE: src/client/rsg-components/PlaygroundError/PlaygroundError.spec.tsx
================================================
import React from 'react';
import { createRenderer } from 'react-test-renderer/shallow';
import { PlaygroundErrorRenderer } from './PlaygroundErrorRenderer';
it('renderer should render message', () => {
const message = 'Hello *world*!';
const renderer = createRenderer();
renderer.render( );
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
================================================
FILE: src/client/rsg-components/PlaygroundError/PlaygroundErrorRenderer.tsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import Styled, { JssInjectedProps } from 'rsg-components/Styled';
import * as Rsg from '../../../typings';
const styles = ({ fontFamily, fontSize, color }: Rsg.Theme) => ({
root: {
margin: 0,
lineHeight: 1.2,
fontSize: fontSize.small,
fontFamily: fontFamily.monospace,
color: color.error,
whiteSpace: 'pre-wrap',
},
});
interface PlaygroundErrorProps extends JssInjectedProps {
message: string;
}
export const PlaygroundErrorRenderer: React.FunctionComponent = ({
classes,
message,
}) => {message} ;
PlaygroundErrorRenderer.propTypes = {
classes: PropTypes.objectOf(PropTypes.string.isRequired).isRequired,
message: PropTypes.string.isRequired,
};
export default Styled(styles)(PlaygroundErrorRenderer);
================================================
FILE: src/client/rsg-components/PlaygroundError/__snapshots__/PlaygroundError.spec.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renderer should render message 1`] = `
Hello *world*!
`;
================================================
FILE: src/client/rsg-components/PlaygroundError/index.ts
================================================
export { default } from 'rsg-components/PlaygroundError/PlaygroundErrorRenderer';
================================================
FILE: src/client/rsg-components/Preview/Preview.spec.tsx
================================================
import React from 'react';
import { render, waitFor } from '@testing-library/react';
import Preview from '.';
import Context, { StyleGuideContextContents } from '../Context';
/* eslint-disable no-console */
const evalInContext = (a: string) =>
// eslint-disable-next-line no-new-func
new Function('require', 'state', 'setState', 'const React = require("react");' + a).bind(
null,
require
);
const code = 'Code: OK ';
const newCode = 'Code: Cancel ';
const context = {
config: {
compilerConfig: {},
},
codeRevision: 0,
} as StyleGuideContextContents;
const Provider = (props: Record) => ;
const console$error = console.error;
const console$clear = console.clear;
afterEach(() => {
console.error = console$error;
console.clear = console$clear;
});
it('should unmount Wrapper component', async () => {
const { unmount, getByTestId } = render(
);
const node = getByTestId('mountNode');
expect(node.innerHTML).toMatch(' expect(node.innerHTML).toBe(''));
});
it('should not fail when Wrapper wasn’t mounted', () => {
const consoleError = jest.fn();
console.error = consoleError;
const { unmount, getByTestId } = render(
);
const node = getByTestId('mountNode');
expect(
consoleError.mock.calls.find((call) =>
call[0].toString().includes('ReferenceError: pizza is not defined')
)
).toBeTruthy();
expect(node.innerHTML).toBe('');
unmount();
expect(node.innerHTML).toBe('');
});
it('should wrap code in Fragment when it starts with <', () => {
console.error = jest.fn();
const { queryAllByRole } = render(
);
// If two buttons weren't wrapped in a Fragment, we'd see an error in console
expect(console.error).not.toHaveBeenCalled();
expect(queryAllByRole('button')).toHaveLength(2);
});
it('should update', () => {
const { rerender, getByText } = render(
);
expect(getByText('Code: OK')).toBeInTheDocument();
rerender(
);
expect(getByText('Code: Cancel')).toBeInTheDocument();
});
it('should handle no code', () => {
console.error = jest.fn();
render(
);
expect(console.error).not.toHaveBeenCalled();
});
it('should handle errors', () => {
const consoleError = jest.fn();
console.error = consoleError;
render(
);
expect(
consoleError.mock.calls.find((call) =>
call[0].toString().includes('SyntaxError: Unexpected token')
)
).toBeTruthy();
});
it('should not clear console on initial mount', () => {
console.clear = jest.fn();
render(
);
expect(console.clear).toHaveBeenCalledTimes(0);
});
it('should clear console on second mount', () => {
console.clear = jest.fn();
render(
);
expect(console.clear).toHaveBeenCalledTimes(1);
});
================================================
FILE: src/client/rsg-components/Preview/Preview.tsx
================================================
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import PlaygroundError from 'rsg-components/PlaygroundError';
import ReactExample from 'rsg-components/ReactExample';
import Context, { StyleGuideContextContents } from 'rsg-components/Context';
import { createRoot, Root } from 'react-dom/client';
const improveErrorMessage = (message: string) =>
message.replace(
'Check the render method of `StateHolder`.',
'Check the code of your example in a Markdown file or in the editor below.'
);
interface PreviewProps {
code: string;
evalInContext(code: string): () => any;
}
interface PreviewState {
error: string | null;
}
export default class Preview extends Component {
public static propTypes = {
code: PropTypes.string.isRequired,
evalInContext: PropTypes.func.isRequired,
};
public static contextType = Context;
private mountNode: Element | null = null;
private reactRoot: Root | null = null;
private timeoutId: number | null = null;
public state: PreviewState = {
error: null,
};
public componentDidMount() {
// Clear console after hot reload, do not clear on the first load
// to keep any warnings
if ((this.context as StyleGuideContextContents).codeRevision > 0) {
// eslint-disable-next-line no-console
console.clear();
}
this.executeCode();
}
public shouldComponentUpdate(nextProps: PreviewProps, nextState: PreviewState) {
return this.state.error !== nextState.error || this.props.code !== nextProps.code;
}
public componentDidUpdate(prevProps: PreviewProps) {
if (this.props.code !== prevProps.code) {
this.executeCode();
}
}
public componentWillUnmount() {
this.unmountPreview();
}
public unmountPreview() {
const self = this;
if (self.timeoutId) {
clearTimeout(self.timeoutId);
}
const id = setTimeout(() => {
if (self.reactRoot) {
self.reactRoot.unmount();
self.reactRoot = null;
}
});
self.timeoutId = id;
}
private executeCode() {
this.setState({
error: null,
});
const { code } = this.props;
if (!code) {
return;
}
const wrappedComponent: React.FunctionComponentElement = (
);
/* istanbul ignore next */
window.requestAnimationFrame(() => {
if (!this.mountNode) {
return;
}
try {
if (this.reactRoot === null) {
this.reactRoot = createRoot(this.mountNode);
this.reactRoot.render(wrappedComponent);
} else {
this.reactRoot.render(wrappedComponent);
}
} catch (err) {
if (err instanceof Error) {
this.handleError(err);
}
}
});
}
private handleError = (err: Error) => {
this.unmountPreview();
this.setState({
error: improveErrorMessage(err.toString()),
});
console.error(err); // eslint-disable-line no-console
};
private callbackRef = (ref: HTMLDivElement | null) => {
this.mountNode = ref;
if (!this.reactRoot && ref) {
this.reactRoot = createRoot(ref);
}
};
public render() {
const { error } = this.state;
return (
<>
{error && }
>
);
}
}
================================================
FILE: src/client/rsg-components/Preview/index.ts
================================================
export { default } from 'rsg-components/Preview/Preview';
================================================
FILE: src/client/rsg-components/Props/Props.spec.tsx
================================================
/* eslint-disable react/prop-types */
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { parse } from 'react-docgen';
import PropsRenderer, { columns, getRowKey } from './PropsRenderer';
import { unquote, getType, showSpaces, PropDescriptor } from './util';
const propsToArray = (props: any) => Object.keys(props).map(name => ({ ...props[name], name }));
const getText = (node: { innerHTML: string }): string =>
node.innerHTML
.replace(/<\/?(div|li|p).*?>/g, '\n')
.replace(/<.*?>/g, ' ')
.replace(/[\r\n]+/g, '\n')
.replace(/ +/g, ' ')
.trim();
// Test renderers with clean readable snapshot diffs
export default function ColumnsRenderer({ props }: { props: PropDescriptor[] }) {
return (
<>
{props.map((row, rowIdx) => (
{columns.map((col, colIdx) => (
{col.caption}: {col.render(row)}
))}
))}
>
);
}
function renderJs(propTypes: string[], defaultProps: string[] = []) {
const props = parse(
`
import { Component } from 'react';
import PropTypes from 'prop-types';
export default class Cmpnt extends Component {
static propTypes = {
${propTypes.join(',')}
}
static defaultProps = {
${defaultProps.join(',')}
}
render() {
}
}
`,
undefined,
undefined,
{ filename: '' }
);
if (Array.isArray(props)) {
return render(
);
}
return render( );
}
function renderFlow(propsType: string[], defaultProps: string[] = [], preparations: string[] = []) {
const props = parse(
`
// @flow
import * as React from 'react';
${preparations.join(';')}
type Props = {
${propsType.join(',')}
};
export default class Cmpnt extends React.Component {
static defaultProps = {
${defaultProps.join(',')}
}
render() {
}
}
`,
undefined,
undefined,
{ filename: '' }
);
if (Array.isArray(props)) {
return render(
);
}
return render( );
}
function renderTypeScript(
propsType: string[],
defaultProps: string[] = [],
preparations: string[] = []
) {
const props = parse(
`
import * as React from 'react';
${preparations.join(';')}
type Props = {
${propsType.join(';')}
};
export default class Cmpnt extends React.Component {
static defaultProps = {
${defaultProps.join(',')}
}
render() {
}
}
`,
undefined,
undefined,
{ filename: 'Component.tsx' }
);
if (Array.isArray(props)) {
return render(
);
}
return render( );
}
describe('PropsRenderer', () => {
test('should render a table', async () => {
const { findAllByRole } = render(
);
expect((await findAllByRole('columnheader')).map(node => node.textContent)).toEqual([
'Prop name',
'Type',
'Default',
'Description',
]);
expect((await findAllByRole('cell')).map(node => node.textContent)).toEqual([
'color',
'string',
'tomato',
'Butiful',
]);
});
});
describe('props columns', () => {
test('should render PropTypes.string', () => {
const { container } = renderJs(['color: PropTypes.string']);
expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: color
Type: string
Default:
Description:"
`);
});
test('should render PropTypes.string with a default value', () => {
const { container } = renderJs(['color: PropTypes.string'], ['color: "pink"']);
expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: color
Type: string
Default: pink
Description:"
`);
});
test('should render PropTypes.string.isRequired', () => {
const { container } = renderJs(['color: PropTypes.string.isRequired']);
expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: color
Type: string
Default: Required
Description:"
`);
});
test('should render PropTypes.arrayOf', () => {
const { container } = renderJs(['colors: PropTypes.arrayOf(PropTypes.string)']);
expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: colors
Type: string[]
Default:
Description:"
`);
});
test('should render PropTypes.arrayOf(PropTypes.shape)', () => {
const { container } = renderJs([
'foos: PropTypes.arrayOf(PropTypes.shape({bar: PropTypes.number, baz: PropTypes.any}))',
]);
expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: foos
Type: shape[]
Default:
Description:
bar : number
baz : any"
`);
});
test('should render PropTypes.arrayOf(PropTypes.exact)', () => {
const { container } = renderJs([
'foos: PropTypes.arrayOf(PropTypes.exact({bar: PropTypes.number, baz: PropTypes.any}))',
]);
expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: foos
Type: exact[]
Default:
Description:
bar : number
baz : any"
`);
});
test('should render PropTypes.instanceOf', () => {
const { container } = renderJs(['num: PropTypes.instanceOf(Number)']);
expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: num
Type: Number
Default:
Description:"
`);
});
test('should render PropTypes.shape', () => {
const { container } = renderJs([
'foo: PropTypes.shape({bar: PropTypes.number.isRequired, baz: PropTypes.any})',
]);
expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: foo
Type: shape
Default:
Description:
bar : number — Required
baz : any"
`);
});
test('should render PropTypes.exact', () => {
const { container } = renderJs([
'foo: PropTypes.exact({bar: PropTypes.number.isRequired, baz: PropTypes.any})',
]);
expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: foo
Type: exact
Default:
Description:
bar : number — Required
baz : any"
`);
});
test('should render PropTypes.shape with formatted defaultProps', () => {
const { getByText } = renderJs(
[
`
foo: PropTypes.shape({
bar: PropTypes.number.isRequired,
baz: PropTypes.any,
})
`,
],
[
`
foo: {
bar: 123, baz() {
return 'foo';
},
bing() {
return 'badaboom';
},
trotskij: () => 1935,
qwarc: { si: 'señor', },
}
`,
]
);
expect(getByText('Shape').title).toMatchInlineSnapshot(`
"{
\\"bar\\": 123,
\\"qwarc\\": {
\\"si\\": \\"señor\\"
}
}"
`);
});
test('should render PropTypes.shape defaultProps, falling back to Object', () => {
const { container, getByText } = renderJs(
[
`
foo: PropTypes.shape({
bar: PropTypes.number.isRequired,
baz: PropTypes.any,
})
`,
],
[
`
foo: somethingThatDoesntExist
`,
]
);
expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: foo
Type: shape
Default: Shape
Description:
bar : number — Required
baz : any"
`);
// FIXME: This doesn't look correct, where's foo?
expect(getByText('Shape').title).toMatchInlineSnapshot(`"somethingThatDoesntExist"`);
});
test('should render PropTypes.shape with description', () => {
const { container } = renderJs([
`foo: PropTypes.shape({
/**
* Number
*/
bar: PropTypes.number.isRequired,
/** Any */
baz: PropTypes.any
})`,
]);
expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: foo
Type: shape
Default:
Description:
bar : number — Required — Number
baz : any — Any"
`);
});
test('should render PropTypes.exact with description', () => {
const { container } = renderJs([
`foo: PropTypes.exact({
/**
* Number
*/
bar: PropTypes.number.isRequired,
/** Any */
baz: PropTypes.any
})`,
]);
expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: foo
Type: exact
Default:
Description:
bar : number — Required — Number
baz : any — Any"
`);
});
test('should render PropTypes.objectOf', () => {
const { container } = renderJs(['colors: PropTypes.objectOf(PropTypes.string)']);
expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: colors
Type: {string}
Default:
Description:"
`);
});
test('should render PropTypes.objectOf(PropTypes.shape)', () => {
const { container } = renderJs([
`colors: PropTypes.objectOf(
PropTypes.shape({
bar: PropTypes.number.isRequired,
baz: PropTypes.any
})
)`,
]);
expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: colors
Type: {shape}
Default:
Description:
bar : number — Required
baz : any"
`);
});
test('should render PropTypes.objectOf(PropTypes.exact)', () => {
const { container } = renderJs([
`colors: PropTypes.objectOf(
PropTypes.exact({
bar: PropTypes.number.isRequired,
baz: PropTypes.any
})
)`,
]);
expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: colors
Type: {exact}
Default:
Description:
bar : number — Required
baz : any"
`);
});
test('should render PropTypes.oneOf', () => {
const { container } = renderJs(['size: PropTypes.oneOf(["small", "normal", "large"])']);
expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: size
Type: enum
Default:
Description:
One of: small , normal , large"
`);
});
test('should render PropTypes.oneOfType', () => {
const { container } = renderJs([
'union: PropTypes.oneOfType([PropTypes.string, PropTypes.number])',
]);
expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: union
Type: union
Default:
Description:
One of type: string , number"
`);
});
test('should render description in Markdown', () => {
const { container } = renderJs(['/**\n * Label\n */\ncolor: PropTypes.string']);
expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: color
Type: string
Default:
Description:
Label"
`);
});
test('should render unknown proptype for a prop when a relevant proptype is not assigned', () => {
const { container } = renderJs([], ['color: "pink"']);
expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: color
Type:
Default: pink
Description:"
`);
});
test('should render function body in tooltip', () => {
const { getByText } = renderJs(['fn: PropTypes.func'], ['fn: (e) => console.log(e)']);
expect(getByText('Function').title).toMatchInlineSnapshot(`"(e) => console.log(e)"`);
});
test('should render function defaultValue as code when undefined', () => {
const { container } = renderJs(['fn: PropTypes.func'], ['fn: undefined']);
expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: fn
Type: func
Default: undefined
Description:"
`);
});
test('should render function defaultValue as code when null', () => {
const { container } = renderJs(['fn: PropTypes.func'], ['fn: null']);
expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: fn
Type: func
Default: null
Description:"
`);
});
test('should render arguments from JsDoc tags', () => {
const props: any = [
{
name: 'size',
type: {
name: 'number',
},
required: false,
description: 'Test description',
tags: {
arg: [
{
name: 'Foo',
description: 'Converts foo to bar',
type: { type: 'NameExpression', name: 'Array' },
},
],
param: [
{
name: 'Bar',
},
],
},
},
];
const { container } = render( );
expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: size
Type: number
Default:
Description:
Test description
Arguments
Foo : Array — Converts foo to bar Bar"
`);
});
test('should render return from JsDoc tags', () => {
const getProps = (tag: string): any => [
{
name: 'size',
type: {
name: 'number',
},
required: false,
description: 'Test description',
tags: {
[tag]: [
{
title: 'Foo',
description: 'Returns foo from bar',
type: { type: 'NameExpression', name: 'Array' },
},
],
},
},
];
const { container: returnContainer } = render( );
expect(getText(returnContainer)).toMatchInlineSnapshot(`
"Prop name: size
Type: number
Default:
Description:
Test description
Returns Array — Returns foo from bar"
`);
const { container: returnsContainer } = render( );
expect(getText(returnsContainer)).toMatchInlineSnapshot(`
"Prop name: size
Type: number
Default:
Description:
Test description
Returns Array — Returns foo from bar"
`);
});
test('should render name as deprecated when tag deprecated is present', () => {
const props: any[] = [
{
name: 'size',
type: {
name: 'number',
},
required: false,
description: 'Test description',
tags: {
deprecated: [
{
title: 'deprecated',
description: 'Do not use.',
},
],
},
},
];
const { container } = render( );
expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: size
Type: number
Default:
Description:
Test description
Deprecated: Do not use."
`);
});
describe.each([
[
'flowType',
renderFlow,
{ enum: { declaration: "type MyEnum = 'One' | 'Two'", expect: { type: 'enum' } } },
],
[
'TypeScript',
renderTypeScript,
{ enum: { declaration: 'enum MyEnum { One, Two }', expect: { type: 'MyEnum' } } },
],
])('%s', (_, renderFn, options) => {
test('should render type string', () => {
const { container } = renderFn(['foo: string']);
expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: foo
Type: string
Default: Required
Description:"
`);
});
test('should render optional type string', () => {
const { container } = renderFn(['foo?: string']);
expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: foo
Type: string
Default:
Description:"
`);
});
test('should render type string with a default value', () => {
const { container } = renderFn(['foo?: string'], ['foo: "bar"']);
expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: foo
Type: string
Default: bar
Description:"
`);
});
test('should render object type with body in tooltip', () => {
const { container, getByRole } = renderFn(['foo: { bar: string }']);
fireEvent.focus(getByRole('button'));
expect(getByRole('button')).toHaveTextContent('object');
expect(container.querySelector('[data-tippy-root]')).toHaveTextContent('{ bar: string }');
});
test('should render function type with body in tooltip', () => {
const { container, getByRole } = renderFn(['foo: () => void']);
fireEvent.focus(getByRole('button'));
expect(getByRole('button')).toHaveTextContent('function');
expect(container.querySelector('[data-tippy-root]')).toHaveTextContent('() => void');
});
test('should render union type with body in tooltip', () => {
const { container, getByRole } = renderFn(['foo: "bar" | number']);
fireEvent.focus(getByRole('button'));
expect(getByRole('button')).toHaveTextContent('union');
expect(container.querySelector('[data-tippy-root]')).toHaveTextContent('"bar" | number');
});
test('should render enum type', () => {
const { container } = renderFn(['foo: MyEnum'], [], [options.enum.declaration]);
if (options.enum.expect.type === 'enum') {
expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: foo
Type: ${options.enum.expect.type}
Default: Required
Description:
One of: One , Two"
`);
} else {
expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: foo
Type: ${options.enum.expect.type}
Default: Required
Description:"
`);
}
});
test('should render tuple type with body in tooltip', () => {
const { container, getByRole } = renderFn(['foo: ["bar", number]']);
fireEvent.focus(getByRole('button'));
expect(getByRole('button')).toHaveTextContent('tuple');
expect(container.querySelector('[data-tippy-root]')).toHaveTextContent('["bar", number]');
});
test('should render custom class type', () => {
const { container } = renderFn(['foo: React.ReactNode']);
expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: foo
Type: React.ReactNode
Default: Required
Description:"
`);
});
test('should render unknown when a relevant prop type is not assigned', () => {
const { container } = renderFn([], ['color: "pink"']);
expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: color
Type:
Default: pink
Description:"
`);
});
test('should render literal type', () => {
const { container } = renderFn(['foo: 1']);
expect(getText(container)).toMatchInlineSnapshot(`
"Prop name: foo
Type: 1
Default: Required
Description:"
`);
});
});
});
describe('unquote', () => {
test('should remove double quotes around the string', () => {
const result = unquote('"foo"');
expect(result).toBe('foo');
});
test('should remove single quotes around the string', () => {
const result = unquote("'foo'");
expect(result).toBe('foo');
});
test('should not remove quotes in the middle of the string', () => {
const result = unquote('foo"bar');
expect(result).toBe('foo"bar');
});
});
describe('getType', () => {
test('should return not .type but .flowType property', () => {
const result = getType({
type: 'foo',
flowType: 'bar',
} as any);
expect(result).toBe('bar');
});
test('should return not .type but .tsType property', () => {
const result = getType({
type: 'foo',
tsType: 'bar',
} as any);
expect(result).toBe('bar');
});
test('should return .type property', () => {
const result = getType({
type: 'foo',
} as any);
expect(result).toBe('foo');
});
});
describe('showSpaces', () => {
test('should replace leading and trailing spaces with a visible character', () => {
const result = showSpaces(' pizza ');
expect(result).toBe('␣pizza␣');
});
});
describe('getRowKey', () => {
test('should return type name', () => {
const result = getRowKey({ name: 'number' });
expect(result).toBe('number');
});
});
================================================
FILE: src/client/rsg-components/Props/PropsRenderer.tsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import Arguments from 'rsg-components/Arguments';
import Argument from 'rsg-components/Argument';
import JsDoc from 'rsg-components/JsDoc';
import Markdown from 'rsg-components/Markdown';
import Name from 'rsg-components/Name';
import Para from 'rsg-components/Para';
import Table from 'rsg-components/Table';
import renderTypeColumn from './renderType';
import renderExtra from './renderExtra';
import renderDefault from './renderDefault';
import { PropDescriptor } from './util';
function renderDescription(prop: PropDescriptor) {
const { description, tags = {} } = prop;
const extra = renderExtra(prop);
const args = [...(tags.arg || []), ...(tags.argument || []), ...(tags.param || [])];
const returnDocumentation = (tags.return && tags.return[0]) || (tags.returns && tags.returns[0]);
return (
{description &&
}
{extra &&
{extra} }
{args.length > 0 &&
}
{returnDocumentation &&
}
);
}
function renderName(prop: PropDescriptor) {
const { name, tags = {} } = prop;
return {name} ;
}
export function getRowKey(row: { name: string }) {
return row.name;
}
export const columns = [
{
caption: 'Prop name',
render: renderName,
},
{
caption: 'Type',
render: renderTypeColumn,
},
{
caption: 'Default',
render: renderDefault,
},
{
caption: 'Description',
render: renderDescription,
},
];
interface PropsProps {
props: PropDescriptor[];
}
const PropsRenderer: React.FunctionComponent = ({ props }) => {
return ;
};
PropsRenderer.propTypes = {
props: PropTypes.array.isRequired,
};
export default PropsRenderer;
================================================
FILE: src/client/rsg-components/Props/index.ts
================================================
export { default } from 'rsg-components/Props/PropsRenderer';
================================================
FILE: src/client/rsg-components/Props/renderDefault.tsx
================================================
import React from 'react';
import Text from 'rsg-components/Text';
import Code from 'rsg-components/Code';
import { showSpaces, unquote, PropDescriptor } from './util';
const defaultValueBlacklist = ['null', 'undefined'];
export default function renderDefault(prop: PropDescriptor): React.ReactNode {
// Workaround for issue https://github.com/reactjs/react-docgen/issues/221
// If prop has defaultValue it can not be required
if (prop.defaultValue) {
const defaultValueString = showSpaces(unquote(String(prop.defaultValue.value)));
if (prop.type || prop.flowType || prop.tsType) {
const propName = prop.type
? prop.type.name
: prop.flowType
? prop.flowType.type
: prop.tsType && prop.tsType.type;
if (defaultValueBlacklist.indexOf(prop.defaultValue.value) > -1) {
return {defaultValueString};
} else if (propName === 'func' || propName === 'function') {
return (
Function
);
} else if (propName === 'shape' || propName === 'object') {
try {
// We eval source code to be able to format the defaultProp here. This
// can be considered safe, as it is the source code that is evaled,
// which is from a known source and safe by default
// eslint-disable-next-line no-eval
const object = eval(`(${prop.defaultValue.value})`);
return (
Shape
);
} catch (e) {
// eval will throw if it contains a reference to a property not in the
// local scope. To avoid any breakage we fall back to rendering the
// prop without any formatting
return (
Shape
);
}
}
}
return {defaultValueString};
} else if (prop.required) {
return (
Required
);
}
return '';
}
================================================
FILE: src/client/rsg-components/Props/renderExtra.tsx
================================================
import React from 'react';
import Group from 'react-group';
import Type from 'rsg-components/Type';
import Code from 'rsg-components/Code';
import Name from 'rsg-components/Name';
import Markdown from 'rsg-components/Markdown';
import { PropTypeDescriptor } from 'react-docgen';
import { unquote, getType, showSpaces, PropDescriptor, TypeDescriptor } from './util';
import renderDefault from './renderDefault';
import { renderType } from './renderType';
function renderEnum(type: PropTypeDescriptor | TypeDescriptor): React.ReactNode {
if (!Array.isArray(type.value)) {
return {type.value} ;
}
const values = type.value.map(({ value }) => (
{showSpaces(unquote(value))}
));
return (
One of: {values}
);
}
function renderUnion(type: PropTypeDescriptor | TypeDescriptor): React.ReactNode {
if (!Array.isArray(type.value)) {
return {type.value} ;
}
const values = type.value.map((value, index) => (
{renderType(value)}
));
return (
One of type: {values}
);
}
function renderShape(props: Record) {
return Object.keys(props).map(name => {
const prop = props[name];
const defaultValue = renderDefault(prop);
const description = prop.description;
return (
{name}
{': '}
{renderType(prop)}
{defaultValue && ' — '}
{defaultValue}
{description && ' — '}
{description && }
);
});
}
export default function renderExtra(prop: PropDescriptor): React.ReactNode {
const type = getType(prop);
if (!type) {
return null;
}
switch (type.name) {
case 'enum':
return renderEnum(type);
case 'union':
return renderUnion(type);
case 'shape':
return prop.type && renderShape(prop.type.value);
case 'exact':
return prop.type && renderShape(prop.type.value);
case 'arrayOf':
if (type.value.name === 'shape' || type.value.name === 'exact') {
return prop.type && renderShape(prop.type.value.value);
}
return null;
case 'objectOf':
if (type.value.name === 'shape' || type.value.name === 'exact') {
return prop.type && renderShape(prop.type.value.value);
}
return null;
default:
return null;
}
}
================================================
FILE: src/client/rsg-components/Props/renderType.tsx
================================================
import React from 'react';
import { PropTypeDescriptor } from 'react-docgen';
import Type from 'rsg-components/Type';
import ComplexType from 'rsg-components/ComplexType';
import { getType, PropDescriptor, TypeDescriptor } from './util';
interface ExtendedPropTypeDescriptor extends Omit {
name: string;
}
export function renderType(type: ExtendedPropTypeDescriptor): string {
if (!type) {
return 'unknown';
}
const { name } = type;
switch (name) {
case 'arrayOf':
return `${type.value.name}[]`;
case 'objectOf':
return `{${renderType(type.value)}}`;
case 'instanceOf':
return type.value;
default:
return name;
}
}
function renderAdvancedType(type: PropTypeDescriptor | TypeDescriptor): React.ReactNode {
switch (type.name) {
case 'enum':
return {type.name} ;
case 'literal':
return {type.value} ;
case 'signature':
return ;
case 'union':
case 'tuple':
return ;
default:
return {(type as any).raw || (type as any).name} ;
}
}
export default function renderTypeColumn(prop: PropDescriptor): React.ReactNode {
const type = getType(prop);
if (!type) {
return null;
}
if (prop.flowType || prop.tsType) {
return renderAdvancedType(type);
}
return {renderType(type)} ;
}
================================================
FILE: src/client/rsg-components/Props/util.ts
================================================
import { PropDescriptor as BasePropDescriptor, PropTypeDescriptor } from 'react-docgen';
/**
* Remove quotes around given string.
*/
export function unquote(string?: string): string | undefined {
return string && string.replace(/^['"]|['"]$/g, '');
}
export interface PropDescriptor extends BasePropDescriptor {
flowType?: TypeDescriptor;
tsType?: TypeDescriptor;
}
/**
* Return prop type object.
*
* @param {object} prop
* @returns {object}
*/
export function getType(prop: PropDescriptor): PropTypeDescriptor | TypeDescriptor | undefined {
if (prop.flowType) {
if (
prop.flowType.name === 'union' &&
prop.flowType.elements.every((elem: { name: string }) => elem.name === 'literal')
) {
return {
...prop.flowType,
name: 'enum',
value: prop.flowType.elements,
};
}
return prop.flowType;
}
if (prop.tsType) {
return prop.tsType;
}
return prop.type;
}
/**
* Show starting and ending whitespace around given string.
*/
export function showSpaces(string?: string): string | undefined {
return string && string.replace(/^\s|\s$/g, '␣');
}
export interface TypeEnumDescriptor {
name: 'enum';
type: string;
value: TypeDescriptor[];
}
interface TypeLiteralDescriptor {
name: 'literal';
type: string;
value: string;
}
interface TypeSignatureDescriptor {
name: 'signature';
type: string;
raw: string;
value: string;
}
interface TypeUnionDescriptor {
name: 'union' | 'tuple';
elements: TypeDescriptor[];
type: string;
raw: string;
value?: string;
}
export type TypeDescriptor =
| TypeEnumDescriptor
| TypeLiteralDescriptor
| TypeSignatureDescriptor
| TypeUnionDescriptor;
================================================
FILE: src/client/rsg-components/ReactComponent/ReactComponent.spec.tsx
================================================
import React from 'react';
import { render } from '@testing-library/react';
import ReactComponent from './ReactComponent';
import slots from '../slots';
import Context from '../Context';
import { DisplayModes } from '../../consts';
import * as Rsg from '../../../typings';
const context = {
config: {
pagePerSection: false,
},
displayMode: DisplayModes.all,
slots: slots(),
};
const Provider = (props: any) => ;
const evalInContext = (a: string) =>
// eslint-disable-next-line no-new-func
new Function('require', 'const React = require("react");' + a).bind(null, require);
const component = {
name: 'Foo',
visibleName: 'Foo',
slug: 'foo',
href: '#foo',
pathLine: 'foo/bar.js',
props: {
description: 'Bar',
methods: [],
examples: [],
},
metadata: {},
};
const componentWithEverything: Rsg.Component = {
name: 'Foo',
visibleName: 'Foo',
slug: 'foo',
pathLine: 'foo/bar.js',
props: {
description: 'Bar',
methods: [
{
name: 'set',
params: [
{
name: 'newValue',
description: 'New value for the counter.',
type: { type: 'NameExpression', name: 'Number' },
},
],
returns: null,
description: 'Sets the counter to a particular value.',
},
],
examples: [
{
type: 'code',
content: 'Code: OK ',
evalInContext,
},
{
type: 'markdown',
content: 'Markdown: Hello *world*!',
},
],
},
metadata: {
tags: ['one', 'two'],
},
};
test('should render an example placeholder', () => {
const { getByText } = render(
);
expect(getByText(/add examples to this component/i)).toBeInTheDocument();
});
test('should render examples', () => {
const { getByText } = render(
);
expect(getByText(/code: ok/i)).toBeInTheDocument();
expect(getByText(/markdown: hello/i)).toBeInTheDocument();
});
test('should render usage closed by default when usageMode is "collapse"', () => {
const { getByText } = render(
);
expect(getByText(/props & methods/i)).toHaveAttribute('aria-pressed', 'false');
});
test('should render usage opened by default when usageMode is "expand"', () => {
const { getByText } = render(
);
expect(getByText(/props & methods/i)).toHaveAttribute('aria-pressed', 'true');
});
test('should not render usage when usageMode is "hide"', () => {
const { queryByText } = render(
);
expect(queryByText(/props & methods/i)).not.toBeInTheDocument();
});
test('should not render anything when component has no name', () => {
const { container } = render(
);
expect(container).toBeEmptyDOMElement();
});
test('should not render component in isolation mode by default', () => {
const { getByLabelText } = render(
);
expect(getByLabelText(/open isolated/i)).toBeInTheDocument();
});
test('should render component in isolation mode', () => {
const { getByLabelText } = render(
);
expect(getByLabelText(/show all components/i)).toBeInTheDocument();
});
test('should prefix description with deprecated label when @deprecated is present in tags', () => {
const { getByText } = render(
);
expect(getByText(/deprecated:/i)).toBeInTheDocument();
});
================================================
FILE: src/client/rsg-components/ReactComponent/ReactComponent.tsx
================================================
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Examples from 'rsg-components/Examples';
import SectionHeading from 'rsg-components/SectionHeading';
import JsDoc from 'rsg-components/JsDoc';
import Markdown from 'rsg-components/Markdown';
import Slot from 'rsg-components/Slot';
import ReactComponentRenderer from 'rsg-components/ReactComponent/ReactComponentRenderer';
import Context, { StyleGuideContextContents } from 'rsg-components/Context';
import ExamplePlaceholderDefault from 'rsg-components/ExamplePlaceholder';
import { DOCS_TAB_USAGE } from '../slots';
import { DisplayModes, UsageModes } from '../../consts';
import * as Rsg from '../../../typings';
const ExamplePlaceholder =
process.env.STYLEGUIDIST_ENV !== 'production' ? ExamplePlaceholderDefault : () =>
;
interface ReactComponentProps {
component: Rsg.Component;
depth: number;
exampleMode?: string;
usageMode?: string;
}
interface ReactComponentState {
activeTab?: string;
}
export default class ReactComponent extends Component {
public static propTypes = {
component: PropTypes.object.isRequired,
depth: PropTypes.number.isRequired,
exampleMode: PropTypes.string.isRequired,
usageMode: PropTypes.string.isRequired,
};
public static contextType = Context;
public state = {
activeTab: this.props.usageMode === UsageModes.expand ? DOCS_TAB_USAGE : undefined,
};
private handleTabChange = (name: string) => {
this.setState((state) => ({
activeTab: state.activeTab !== name ? name : undefined,
}));
};
public render() {
const { activeTab } = this.state;
const {
displayMode,
config: { pagePerSection },
} = this.context as StyleGuideContextContents;
const { component, depth, usageMode, exampleMode } = this.props;
const { name, visibleName, slug = '-', filepath, pathLine, href } = component;
const { description = '', examples = [], tags = {} } = component.props || {};
if (!name) {
return null;
}
const showUsage = usageMode !== UsageModes.hide;
return (
}
description={description && }
heading={
{visibleName}
}
examples={
examples.length > 0 ? (
) : (
)
}
tabButtons={
showUsage && (
)
}
tabBody={ }
/>
);
}
}
================================================
FILE: src/client/rsg-components/ReactComponent/ReactComponentRenderer.tsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import Pathline from 'rsg-components/Pathline';
import Styled, { JssInjectedProps } from 'rsg-components/Styled';
import * as Rsg from '../../../typings';
const styles = ({ color, fontSize, space }: Rsg.Theme) => ({
root: {
marginBottom: space[6],
},
header: {
marginBottom: space[3],
},
tabs: {
marginBottom: space[3],
},
tabButtons: {
marginBottom: space[1],
},
tabBody: {
overflowX: 'auto',
maxWidth: '100%',
WebkitOverflowScrolling: 'touch',
},
docs: {
color: color.base,
fontSize: fontSize.text,
},
});
interface ReactComponentRendererProps extends JssInjectedProps {
name: string;
heading: React.ReactNode;
filepath?: string;
slug?: string;
pathLine?: string;
tabButtons?: React.ReactNode;
tabBody?: React.ReactNode;
description?: React.ReactNode;
docs?: React.ReactNode;
examples?: React.ReactNode;
isolated?: boolean;
}
export const ReactComponentRenderer: React.FunctionComponent = ({
classes,
name,
heading,
pathLine,
description,
docs,
examples,
tabButtons,
tabBody,
}) => {
return (
{heading}
{pathLine && {pathLine} }
{(description || docs) && (
{description}
{docs}
)}
{tabButtons && (
)}
{examples}
);
};
ReactComponentRenderer.propTypes = {
classes: PropTypes.objectOf(PropTypes.string.isRequired).isRequired,
name: PropTypes.string.isRequired,
heading: PropTypes.any.isRequired,
filepath: PropTypes.string,
pathLine: PropTypes.string,
tabButtons: PropTypes.any,
tabBody: PropTypes.any,
description: PropTypes.any,
docs: PropTypes.any,
examples: PropTypes.any,
isolated: PropTypes.bool,
};
export default Styled(styles)(ReactComponentRenderer);
================================================
FILE: src/client/rsg-components/ReactComponent/index.ts
================================================
export { default } from 'rsg-components/ReactComponent/ReactComponent';
export * from 'rsg-components/ReactComponent/ReactComponent';
================================================
FILE: src/client/rsg-components/ReactExample/ReactExample.spec.tsx
================================================
import { fireEvent, render } from '@testing-library/react';
import React from 'react';
import noop from 'lodash/noop';
import renderer from 'react-test-renderer';
import { createRenderer } from 'react-test-renderer/shallow';
import ReactExample from '.';
const evalInContext = (a: string): (() => any) =>
// eslint-disable-next-line no-new-func
new Function('require', 'const React = require("react");' + a).bind(null, require);
it('should render code', () => {
const testRenderer = createRenderer();
testRenderer.render(
OK '} evalInContext={evalInContext} onError={noop} />
);
expect(testRenderer.getRenderOutput()).toMatchSnapshot();
});
it('should wrap code in Fragment when it starts with <', () => {
const actual = renderer.create(
);
expect(actual.toJSON()).toMatchSnapshot();
});
it('should handle errors', () => {
const onError = jest.fn();
const testRenderer = createRenderer();
testRenderer.render(
);
expect(onError).toHaveBeenCalledTimes(1);
});
it('should set initial state with hooks', () => {
const code = `
const [count, setCount] = React.useState(0);
{count}
`;
const { getByRole } = render(
);
expect(getByRole('button').textContent).toEqual('0');
});
it('should update state with hooks', () => {
const code = `
const [count, setCount] = React.useState(0);
setCount(count+1)}>{count}
`;
const { getByRole } = render(
);
fireEvent.click(getByRole('button'));
expect(getByRole('button').textContent).toEqual('1');
});
================================================
FILE: src/client/rsg-components/ReactExample/ReactExample.tsx
================================================
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { TransformOptions } from 'buble';
import Wrapper from 'rsg-components/Wrapper';
import compileCode from '../../utils/compileCode';
import splitExampleCode from '../../utils/splitExampleCode';
/* eslint-disable react/no-multi-comp */
interface ReactExampleProps {
code: string;
evalInContext(code: string): () => any;
onError(err: Error): void;
compilerConfig?: TransformOptions;
}
export default class ReactExample extends Component {
public static propTypes = {
code: PropTypes.string.isRequired,
evalInContext: PropTypes.func.isRequired,
onError: PropTypes.func.isRequired,
compilerConfig: PropTypes.object,
};
public shouldComponentUpdate(nextProps: ReactExampleProps) {
return this.props.code !== nextProps.code;
}
// Run example code and return the last top-level expression
private getExampleComponent(compiledCode: string): () => any {
return this.props.evalInContext(`
${compiledCode}
`);
}
public render() {
const { code, compilerConfig = {}, onError } = this.props;
const compiledCode = compileCode(code, compilerConfig, onError);
if (!compiledCode) {
return null;
}
const { example } = splitExampleCode(compiledCode);
const ExampleComponent = this.getExampleComponent(example);
const wrappedComponent = (
);
return wrappedComponent;
}
}
================================================
FILE: src/client/rsg-components/ReactExample/__snapshots__/ReactExample.spec.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render code 1`] = `
`;
exports[`should wrap code in Fragment when it starts with < 1`] = `
`;
================================================
FILE: src/client/rsg-components/ReactExample/index.ts
================================================
export { default } from 'rsg-components/ReactExample/ReactExample';
================================================
FILE: src/client/rsg-components/Ribbon/Ribbon.spec.tsx
================================================
import React from 'react';
import { render } from '@testing-library/react';
import Ribbon from './Ribbon';
import Context from '../Context';
const url = 'http://example.com/';
const text = 'Share the repo';
it('should render ribbon if the ribbon is present in the config', () => {
const { getByRole } = render(
);
expect(getByRole('link')).toHaveAttribute('href', url);
});
it('should render ribbon with custom text', () => {
const { getByText } = render(
);
expect(getByText(text)).toBeInTheDocument();
});
it('should not render anything if the ribbon is not present in the config', () => {
const { queryByRole } = render(
);
expect(queryByRole('link')).not.toBeInTheDocument();
});
================================================
FILE: src/client/rsg-components/Ribbon/Ribbon.tsx
================================================
import React from 'react';
import RibbonRenderer from 'rsg-components/Ribbon/RibbonRenderer';
import { useStyleGuideContext } from 'rsg-components/Context';
export default function Ribbon() {
const {
config: { ribbon },
} = useStyleGuideContext();
return ribbon ? : null;
}
================================================
FILE: src/client/rsg-components/Ribbon/RibbonRenderer.tsx
================================================
import React from 'react';
import Styled, { JssInjectedProps } from 'rsg-components/Styled';
import * as Rsg from '../../../typings';
export const styles = ({ color, space, fontSize, fontFamily }: Rsg.Theme) => ({
root: {
position: 'fixed',
top: 0,
right: 0,
width: 149,
height: 149,
zIndex: 999,
},
link: {
fontFamily: fontFamily.base,
position: 'relative',
right: -37,
top: -22,
display: 'block',
width: 190,
padding: [[space[0], space[2]]],
textAlign: 'center',
color: color.ribbonText,
fontSize: fontSize.base,
background: color.ribbonBackground,
textDecoration: 'none',
textShadow: [[0, '-1px', 0, 'rgba(0,0,0,.15)']],
transformOrigin: [[0, 0]],
transform: 'rotate(45deg)',
cursor: 'pointer',
},
});
interface RibbonProps extends JssInjectedProps {
url: string;
text?: string;
}
export const RibbonRenderer: React.FunctionComponent = ({
classes,
url,
text = 'Fork me on GitHub',
}) => {
return (
);
};
export default Styled(styles)(RibbonRenderer);
================================================
FILE: src/client/rsg-components/Ribbon/index.ts
================================================
export { default } from 'rsg-components/Ribbon/Ribbon';
================================================
FILE: src/client/rsg-components/Section/Section.spec.tsx
================================================
import React from 'react';
import { render } from '@testing-library/react';
import Section from 'rsg-components/Section';
import Context from '../Context';
import slots from '../slots';
import { DisplayModes } from '../../consts';
const context = {
config: {
pagePerSection: false,
},
displayMode: DisplayModes.all,
slots: slots(),
} as any;
const Provider = (props: any) => ;
test('should render nested sections', () => {
const { getByTestId } = render(
);
expect(getByTestId('section-outer-section')).toContainElement(
getByTestId('section-nested-section')
);
});
test('should render components inside a section', () => {
const { getByTestId, getByText } = render(
);
expect(getByTestId('section-components')).toContainElement(getByText('components/foo.js'));
expect(getByTestId('section-components')).toContainElement(getByText('components/bar.js'));
});
test('should not render section in isolation mode by default', () => {
const { getByLabelText } = render(
);
expect(getByLabelText(/open isolated/i)).toBeInTheDocument();
});
test('should render section in isolation mode', () => {
const { queryByLabelText } = render(
);
expect(queryByLabelText(/open isolated/i)).toBeNull();
});
================================================
FILE: src/client/rsg-components/Section/Section.tsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import Examples from 'rsg-components/Examples';
import Components from 'rsg-components/Components';
import Sections from 'rsg-components/Sections';
import SectionRenderer from 'rsg-components/Section/SectionRenderer';
import { useStyleGuideContext } from 'rsg-components/Context';
import { DisplayModes } from '../../consts';
import * as Rsg from '../../../typings';
const Section: React.FunctionComponent<{
section: Rsg.Section;
depth: number;
}> = ({ section, depth }) => {
const {
displayMode,
config: { pagePerSection },
} = useStyleGuideContext();
const {
name,
slug,
filepath,
content,
components,
sections,
description,
exampleMode,
usageMode,
} = section;
const contentJsx = Array.isArray(content) ? (
) : null;
const componentsJsx = components && (
);
const sectionsJsx = sections && ;
return (
);
};
Section.propTypes = {
section: PropTypes.any.isRequired,
depth: PropTypes.number.isRequired,
};
export default Section;
================================================
FILE: src/client/rsg-components/Section/SectionRenderer.tsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import Styled, { JssInjectedProps } from 'rsg-components/Styled';
import SectionHeading from 'rsg-components/SectionHeading';
import Markdown from 'rsg-components/Markdown';
import * as Rsg from '../../../typings';
const styles = ({ space }: Rsg.Theme) => ({
root: {
marginBottom: space[4],
},
});
interface SectionRendererProps extends JssInjectedProps {
slug: string;
depth: number;
name?: string;
description?: string;
content?: React.ReactNode;
components?: React.ReactNode;
sections?: React.ReactNode;
isolated?: boolean;
pagePerSection?: boolean;
[prop: string]: any;
}
export const SectionRenderer: React.FunctionComponent = (allProps) => {
const {
classes,
name,
slug,
content,
components,
sections,
depth,
description,
pagePerSection,
} = allProps;
return (
{name && (
{name}
)}
{description && }
{content}
{sections}
{components}
);
};
SectionRenderer.propTypes = {
classes: PropTypes.objectOf(PropTypes.string.isRequired).isRequired,
name: PropTypes.string,
description: PropTypes.string,
slug: PropTypes.string.isRequired,
content: PropTypes.any,
components: PropTypes.any,
sections: PropTypes.any,
isolated: PropTypes.bool,
depth: PropTypes.number.isRequired,
pagePerSection: PropTypes.bool,
};
export default Styled(styles)(SectionRenderer);
================================================
FILE: src/client/rsg-components/Section/index.ts
================================================
export { default } from 'rsg-components/Section/Section';
export * from 'rsg-components/Section/Section';
================================================
FILE: src/client/rsg-components/SectionHeading/SectionHeading.spec.tsx
================================================
import React from 'react';
import renderer from 'react-test-renderer';
import { createRenderer } from 'react-test-renderer/shallow';
import SectionHeading from './index';
import SectionHeadingRenderer from './SectionHeadingRenderer';
describe('SectionHeading', () => {
const FakeToolbar = () => Fake toolbar
;
test('should forward slot properties to the toolbar', () => {
const testRenderer = createRenderer();
testRenderer.render(
A Section
);
expect(testRenderer.getRenderOutput()).toMatchSnapshot();
});
test('render a section heading', () => {
const actual = renderer.create(
}>
A Section
);
expect(actual.toJSON()).toMatchSnapshot();
});
test('render a deprecated section heading', () => {
const actual = renderer.create(
}
deprecated
>
A Section
);
expect(actual.toJSON()).toMatchSnapshot();
});
test('prevent the heading level from exceeding the maximum allowed by the Heading component', () => {
const actual = renderer.create(
}>
A Section
);
expect(actual.toJSON()).toMatchSnapshot();
});
});
================================================
FILE: src/client/rsg-components/SectionHeading/SectionHeading.tsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import Slot from 'rsg-components/Slot';
import SectionHeadingRenderer from 'rsg-components/SectionHeading/SectionHeadingRenderer';
interface SectionHeadingProps {
children?: React.ReactNode;
id: string;
slotName: string;
slotProps: Record;
depth: number;
href?: string;
deprecated?: boolean;
pagePerSection?: boolean;
}
const SectionHeading: React.FunctionComponent = ({
slotName,
slotProps,
children,
id,
href,
...rest
}) => {
return (
}
id={id}
href={href}
{...rest}
>
{children}
);
};
SectionHeading.propTypes = {
children: PropTypes.any,
id: PropTypes.string.isRequired,
slotName: PropTypes.string.isRequired,
slotProps: PropTypes.any.isRequired,
depth: PropTypes.number.isRequired,
deprecated: PropTypes.bool,
pagePerSection: PropTypes.bool,
};
export default SectionHeading;
================================================
FILE: src/client/rsg-components/SectionHeading/SectionHeadingRenderer.tsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'clsx';
import Heading from 'rsg-components/Heading';
import Styled, { JssInjectedProps } from 'rsg-components/Styled';
import * as Rsg from '../../../typings';
const styles = ({ color, space }: Rsg.Theme) => ({
wrapper: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
marginBottom: space[1],
},
toolbar: {
marginLeft: 'auto',
},
sectionName: {
'&:hover, &:active': {
isolate: false,
textDecoration: 'underline',
cursor: 'pointer',
},
},
isDeprecated: {
color: color.light,
'&, &:hover': {
textDecoration: 'line-through',
},
},
});
interface SectionHeadingRendererProps extends JssInjectedProps {
children?: React.ReactNode;
toolbar?: React.ReactNode;
id: string;
href?: string;
depth: number;
deprecated?: boolean;
}
const SectionHeadingRenderer: React.FunctionComponent = ({
classes,
children,
toolbar,
id,
href,
depth,
deprecated,
}) => {
const headingLevel = Math.min(6, depth);
const sectionNameClasses = cx(classes.sectionName, {
[classes.isDeprecated]: deprecated,
});
return (
);
};
SectionHeadingRenderer.propTypes = {
classes: PropTypes.objectOf(PropTypes.string.isRequired).isRequired,
children: PropTypes.any,
toolbar: PropTypes.any,
id: PropTypes.string.isRequired,
href: PropTypes.string,
depth: PropTypes.number.isRequired,
deprecated: PropTypes.bool,
};
export default Styled(styles)(SectionHeadingRenderer);
================================================
FILE: src/client/rsg-components/SectionHeading/__snapshots__/SectionHeading.spec.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SectionHeading prevent the heading level from exceeding the maximum allowed by the Heading component 1`] = `
`;
exports[`SectionHeading render a deprecated section heading 1`] = `
`;
exports[`SectionHeading render a section heading 1`] = `
`;
exports[`SectionHeading should forward slot properties to the toolbar 1`] = `
}
>
A Section
`;
================================================
FILE: src/client/rsg-components/SectionHeading/index.ts
================================================
export { default } from 'rsg-components/SectionHeading/SectionHeading';
================================================
FILE: src/client/rsg-components/Sections/Sections.spec.tsx
================================================
import React from 'react';
import { createRenderer } from 'react-test-renderer/shallow';
import noop from 'lodash/noop';
import Section from '../Section';
import Sections from './Sections';
import StyledSectionsRenderer, { SectionsRenderer } from './SectionsRenderer';
const sections = [
{
name: 'Foo',
content: [
{
type: 'code',
content: 'OK ',
evalInContext: noop,
},
],
components: [],
},
{
name: 'Bar',
content: [
{
type: 'markdown',
content: 'Hello *world*!',
},
],
components: [],
},
{
sections: [
{
name: 'One',
content: [],
},
{
name: 'Two',
content: [],
},
],
},
] as any;
it('should render component renderer', () => {
const renderer = createRenderer();
renderer.render( );
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
it('render should render styled component', () => {
const renderer = createRenderer();
renderer.render(
);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
it('render should render component', () => {
const renderer = createRenderer();
renderer.render(
);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
================================================
FILE: src/client/rsg-components/Sections/Sections.tsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import Section from 'rsg-components/Section';
import SectionsRenderer from 'rsg-components/Sections/SectionsRenderer';
import * as Rsg from '../../../typings';
const Sections: React.FunctionComponent<{
sections: Rsg.Section[];
depth: number;
root?: boolean;
}> = ({ sections, depth }) => {
return (
{sections
.filter(section => !section.externalLink)
.map((section, idx) => (
))}
);
};
Sections.propTypes = {
sections: PropTypes.array.isRequired,
depth: PropTypes.number.isRequired,
root: PropTypes.bool,
};
export default Sections;
================================================
FILE: src/client/rsg-components/Sections/SectionsRenderer.tsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import Styled, { JssInjectedProps } from 'rsg-components/Styled';
const styles = () => ({
// Just default jss-isolate rules
root: {},
});
interface SectionsRendererProps extends JssInjectedProps {
children: React.ReactNode;
}
export const SectionsRenderer: React.FunctionComponent = ({
classes,
children,
}) => {
return ;
};
SectionsRenderer.propTypes = {
classes: PropTypes.objectOf(PropTypes.string.isRequired).isRequired,
children: PropTypes.any,
};
export default Styled(styles)(SectionsRenderer);
================================================
FILE: src/client/rsg-components/Sections/__snapshots__/Sections.spec.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`render should render component 1`] = `
OK",
"evalInContext": [Function],
"type": "code",
},
],
"name": "Foo",
}
}
/>
`;
exports[`render should render styled component 1`] = `
OK",
"evalInContext": [Function],
"type": "code",
},
],
"name": "Foo",
}
}
/>
`;
exports[`should render component renderer 1`] = `
OK",
"evalInContext": [Function],
"type": "code",
},
],
"name": "Foo",
}
}
/>
`;
================================================
FILE: src/client/rsg-components/Sections/index.ts
================================================
export { default } from 'rsg-components/Sections/Sections';
================================================
FILE: src/client/rsg-components/Slot/Slot.spec.tsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import { render, fireEvent } from '@testing-library/react';
import Slot from './Slot';
import Context from '../Context';
const Button = ({ active, children, ...props }: any) => {
return (
{children}
);
};
Button.propTypes = {
active: PropTypes.bool,
children: PropTypes.node,
};
const Button1 = (props: any) => Button1 ;
const Button2 = (props: any) => Button2 ;
const fillsWithIds = [
{
id: 'one',
render: Button1,
},
{
id: 'two',
render: Button2,
},
];
it('should render slots and pass props', () => {
const { getByText, getAllByRole } = render(
);
expect(getByText('Button1')).toBeInTheDocument();
expect(getByText('Button2')).toBeInTheDocument();
expect(getAllByRole('pizza')).toHaveLength(2);
});
it('should render slots in id/render format', () => {
const { getByText } = render(
);
expect(getByText('Button1')).toBeInTheDocument();
expect(getByText('Button2')).toBeInTheDocument();
});
it('should pass active flag to active slot', () => {
const { getByText } = render(
);
expect(getByText('Button1')).toHaveAttribute('aria-current', 'false');
expect(getByText('Button2')).toHaveAttribute('aria-current', 'true');
});
it('should render only active slot if onlyActive=true', () => {
const { queryByText } = render(
);
expect(queryByText('Button1')).not.toBeInTheDocument();
expect(queryByText('Button2')).toBeInTheDocument();
});
it('should pass slot ID to onClick handler', () => {
const onClick = jest.fn();
const { getByText } = render(
);
fireEvent.click(getByText('Button2'));
expect(onClick).toHaveBeenCalledTimes(1);
expect(onClick.mock.calls[0][0]).toBe('two');
});
it('should return null if all slots render null', () => {
const { queryByText } = render(
null],
},
} as any
}
>
);
expect(queryByText('Button1')).not.toBeInTheDocument();
expect(queryByText('Button2')).not.toBeInTheDocument();
});
================================================
FILE: src/client/rsg-components/Slot/Slot.tsx
================================================
// Inspired by https://github.com/camwest/react-slot-fill
import React from 'react';
import PropTypes from 'prop-types';
import { useStyleGuideContext, SlotObject } from 'rsg-components/Context';
interface SlotProps {
name: string;
active?: string;
onlyActive?: boolean;
props?: {
onClick?: (id: string, ...attrs: any[]) => void;
active?: boolean;
name?: string;
[propId: string]: any;
};
className?: string;
}
export default function Slot({ name, active, onlyActive, className, props = {} }: SlotProps) {
const { slots } = useStyleGuideContext();
const fills = slots[name];
if (!fills) {
throw new Error(`Slot "${name}" not found, available slots: ${Object.keys(slots).join(', ')}`);
}
const rendered = fills.map((Fill, index) => {
// { id: 'pizza', render: ({ foo }) => {foo}
}
const { id, render } = Fill as SlotObject;
let fillProps = props;
if (id && render) {
// Render only specified fill
if (onlyActive && id !== active) {
return null;
}
// eslint-disable-next-line react/prop-types
const { onClick } = props;
fillProps = {
...props,
name: id,
// Set active prop to active fill
active: active ? id === active : undefined,
// Pass fill ID to onClick event handler
onClick: onClick && ((...attrs) => onClick(id, ...attrs)),
};
const Render = render;
return ;
}
const FillAsComponent = Fill as React.FunctionComponent;
return ;
});
const filtered = rendered.filter(Boolean);
if (filtered.length === 0) {
return null;
}
return {filtered}
;
}
Slot.propTypes = {
name: PropTypes.string.isRequired,
active: PropTypes.string,
onlyActive: PropTypes.bool,
props: PropTypes.object,
className: PropTypes.string,
};
================================================
FILE: src/client/rsg-components/Slot/index.ts
================================================
export { default } from 'rsg-components/Slot/Slot';
================================================
FILE: src/client/rsg-components/StyleGuide/StyleGuide.spec.tsx
================================================
import React from 'react';
import { render, within } from '@testing-library/react';
import StyleGuide, { StyleGuideProps } from './StyleGuide';
import slots from '../slots';
import { DisplayModes } from '../../consts';
import * as Rsg from '../../../typings';
const sections: Rsg.Section[] = [
{
exampleMode: 'collapse',
usageMode: 'collapse',
slug: 'section',
components: [
{
name: 'Foo',
visibleName: 'Foo',
href: '#foo',
slug: 'foo',
pathLine: 'components/foo.js',
filepath: 'components/foo.js',
props: {
description: 'Foo foo',
},
},
{
name: 'Bar',
visibleName: 'Bar',
href: '#bar',
slug: 'bar',
pathLine: 'components/bar.js',
filepath: 'components/bar.js',
props: {
description: 'Bar bar',
},
},
],
},
];
const config = {
title: 'HelloStyleGuide',
version: '1.0.0',
showSidebar: true,
} as Rsg.ProcessedStyleguidistConfig;
const defaultProps: StyleGuideProps = {
codeRevision: 1,
cssRevision: '1',
config,
pagePerSection: false,
sections: [],
allSections: [],
slots: slots(),
patterns: ['components/**.js'],
};
test('should render components', () => {
const { getByText } = render(
);
expect(getByText('components/foo.js')).toBeInTheDocument();
expect(getByText('components/bar.js')).toBeInTheDocument();
});
test('should render welcome screen', () => {
const { getByText } = render( );
expect(getByText('Welcome to React Styleguidist!')).toBeInTheDocument();
});
test('should render a sidebar if showSidebar is not set', () => {
const { getByTestId } = render(
);
const sidebar = within(getByTestId('sidebar'));
const links = sidebar.getAllByRole('link');
expect(links.map((node: any) => node.href)).toEqual([
'http://localhost/#foo',
'http://localhost/#bar',
]);
expect(links.map(node => node.textContent)).toEqual(['Foo', 'Bar']);
});
test('should not render a sidebar if showSidebar is false', () => {
const { queryByTestId } = render(
);
expect(queryByTestId('sidebar')).not.toBeInTheDocument();
});
test('should not render a sidebar in isolation mode', () => {
const { queryByTestId } = render(
);
expect(queryByTestId('sidebar')).not.toBeInTheDocument();
});
test('should render a sidebar if pagePerSection is true', () => {
const { getByTestId } = render(
);
expect(getByTestId('sidebar')).toBeInTheDocument();
});
describe('error handling', () => {
const console$error = console.error;
beforeAll(() => {
console.error = jest.fn();
});
afterAll(() => {
console.error = console$error;
});
test('should render an error when componentDidCatch() is triggered', () => {
const { getByText } = render(
);
expect(getByText(/Page not found/i)).toBeInTheDocument();
});
});
================================================
FILE: src/client/rsg-components/StyleGuide/StyleGuide.tsx
================================================
import React, { Component } from 'react';
import TableOfContents from 'rsg-components/TableOfContents';
import StyleGuideRenderer from 'rsg-components/StyleGuide/StyleGuideRenderer';
import Sections from 'rsg-components/Sections';
import Welcome from 'rsg-components/Welcome';
import Error from 'rsg-components/Error';
import NotFound from 'rsg-components/NotFound';
import Context from 'rsg-components/Context';
import { HOMEPAGE } from '../../../scripts/consts';
import { DisplayModes } from '../../consts';
import * as Rsg from '../../../typings';
/**
* This function will return true, if the sidebar should be visible and false otherwise.
*
* These sorted conditions (highest precedence first) define the visibility
* state of the sidebar.
*
* - Sidebar is hidden for isolated example views
* - Sidebar is always visible when pagePerSection
* - Sidebar is hidden when showSidebar is set to false
* - Sidebar is visible when showSidebar is set to true for non-isolated views
*
* @param {string} displayMode
* @param {boolean} showSidebar
* @param {boolean} pagePerSection
* @returns {boolean}
*/
function hasSidebar(displayMode: string | undefined, showSidebar: boolean): boolean {
return displayMode === DisplayModes.notFound || (showSidebar && displayMode === DisplayModes.all);
}
export interface StyleGuideProps {
codeRevision: number;
cssRevision: string;
config: Rsg.ProcessedStyleguidistConfig;
slots: any;
sections: Rsg.Section[];
welcomeScreen?: boolean;
patterns?: string[];
displayMode?: string;
allSections?: Rsg.Section[];
pagePerSection?: boolean;
}
interface StyleGuideState {
error: Error | boolean;
info: React.ErrorInfo | null;
}
export default class StyleGuide extends Component {
public state = {
error: false,
info: null,
};
public componentDidCatch(error: Error, info: React.ErrorInfo) {
this.setState({
error,
info,
});
}
public render() {
const { error, info }: StyleGuideState = this.state;
const {
config,
sections,
welcomeScreen,
patterns,
displayMode = DisplayModes.all,
allSections,
pagePerSection,
codeRevision,
cssRevision,
slots,
} = this.props;
if (error && info) {
return ;
}
if (welcomeScreen && patterns) {
return ;
}
return (
) : null
}
hasSidebar={hasSidebar(displayMode, config.showSidebar)}
>
{sections.length ? : }
);
}
}
================================================
FILE: src/client/rsg-components/StyleGuide/StyleGuideRenderer.tsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import Logo from 'rsg-components/Logo';
import Markdown from 'rsg-components/Markdown';
import Styled, { JssInjectedProps } from 'rsg-components/Styled';
import cx from 'clsx';
import Ribbon from 'rsg-components/Ribbon';
import Version from 'rsg-components/Version';
import * as Rsg from '../../../typings';
const styles = ({ color, fontFamily, fontSize, sidebarWidth, mq, space, maxWidth }: Rsg.Theme) => ({
root: {
minHeight: '100vh',
backgroundColor: color.baseBackground,
},
hasSidebar: {
paddingLeft: sidebarWidth,
[mq.small]: {
paddingLeft: 0,
},
},
content: {
maxWidth,
padding: [[space[2], space[4]]],
margin: [[0, 'auto']],
[mq.small]: {
padding: space[2],
},
display: 'block',
},
sidebar: {
backgroundColor: color.sidebarBackground,
border: [[color.border, 'solid']],
borderWidth: [[0, 1, 0, 0]],
position: 'fixed',
top: 0,
left: 0,
bottom: 0,
width: sidebarWidth,
overflow: 'auto',
WebkitOverflowScrolling: 'touch',
[mq.small]: {
position: 'static',
width: 'auto',
borderWidth: [[1, 0, 0, 0]],
paddingBottom: space[0],
},
},
logo: {
padding: space[2],
borderBottom: [[1, color.border, 'solid']],
},
footer: {
display: 'block',
color: color.light,
fontFamily: fontFamily.base,
fontSize: fontSize.small,
},
});
interface StyleGuideRendererProps extends JssInjectedProps {
title: string;
version?: string;
homepageUrl: string;
children: React.ReactNode;
toc?: React.ReactNode;
hasSidebar?: boolean;
}
export const StyleGuideRenderer: React.FunctionComponent = ({
classes,
title,
version,
homepageUrl,
children,
toc,
hasSidebar,
}) => {
return (
{children}
{hasSidebar && (
{title}
{version && {version} }
{toc}
)}
);
};
StyleGuideRenderer.propTypes = {
classes: PropTypes.objectOf(PropTypes.string.isRequired).isRequired,
title: PropTypes.string.isRequired,
version: PropTypes.string,
homepageUrl: PropTypes.string.isRequired,
children: PropTypes.any.isRequired,
toc: PropTypes.any.isRequired,
hasSidebar: PropTypes.bool,
};
export default Styled(styles)(StyleGuideRenderer);
================================================
FILE: src/client/rsg-components/StyleGuide/index.ts
================================================
export { default } from 'rsg-components/StyleGuide/StyleGuide';
================================================
FILE: src/client/rsg-components/Styled/Styled.spec.tsx
================================================
import React, { Component } from 'react';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import Styled, { JssInjectedProps } from './Styled';
import Context from '../Context';
const context = {
config: {
theme: {},
styles: {},
},
};
const Provider = (props: any) => ;
/* eslint-disable react/prefer-stateless-function, react/prop-types */
const styles = () => ({
foo: {
color: 'red',
},
});
interface MockProps extends JssInjectedProps {
testId?: string;
}
class TestRenderer extends Component {
public render() {
return
;
}
}
test('should set displayName', () => {
const WrappedComponent = Styled(styles)(TestRenderer);
expect(WrappedComponent.displayName).toBe('Styled(Test)');
});
test('should wrap a component and pass props and classes', () => {
const WrappedComponent = Styled(styles)(TestRenderer);
const { getByTestId } = render(
);
expect(getByTestId('element')).toHaveAttribute('class', expect.stringMatching(/^rsg--foo-\d+$/));
});
================================================
FILE: src/client/rsg-components/Styled/Styled.tsx
================================================
import React, { Component, ComponentType } from 'react';
import { Styles, StyleSheet, Classes } from 'jss';
import Context, { StyleGuideContextContents } from 'rsg-components/Context';
import createStyleSheet from '../../styles/createStyleSheet';
import * as Rsg from '../../../typings';
export interface JssInjectedProps {
classes: Classes;
}
export default function StyleHOC(
styles: (t: Rsg.Theme) => Styles
): (WrappedComponent: ComponentType) => ComponentType> {
return (WrappedComponent: ComponentType) => {
const componentName = WrappedComponent.name.replace(/Renderer$/, '');
return class extends Component> {
public static displayName = `Styled(${componentName})`;
public static contextType = Context;
private sheet: StyleSheet;
public constructor(
props: Omit,
context: StyleGuideContextContents
) {
super(props, context);
this.sheet = createStyleSheet(
styles,
// the protection here is useful for tests
context.config || {},
componentName,
context.cssRevision
);
this.sheet.update(props).attach();
}
public componentDidUpdate(nextProps: P) {
this.sheet.update(nextProps);
}
public render() {
return ;
}
};
};
}
================================================
FILE: src/client/rsg-components/Styled/index.ts
================================================
export { default } from 'rsg-components/Styled/Styled';
export * from 'rsg-components/Styled/Styled';
================================================
FILE: src/client/rsg-components/TabButton/TabButton.spec.tsx
================================================
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import TabButton from './index';
test('should call onClick handler when the button is clicked', () => {
const onClick = jest.fn();
const { getByText } = render(
Pizza
);
fireEvent.click(getByText(/pizza/i));
expect(onClick).toBeCalledTimes(1);
});
================================================
FILE: src/client/rsg-components/TabButton/TabButtonRenderer.tsx
================================================
import React from 'react';
import Styled, { JssInjectedProps } from 'rsg-components/Styled';
import { Styles } from 'jss';
import cx from 'clsx';
import * as Rsg from '../../../typings';
export const styles = ({
space,
color,
fontFamily,
fontSize,
buttonTextTransform,
}: Rsg.Theme): Styles => ({
button: {
padding: [[space[1], 0]],
fontFamily: fontFamily.base,
fontSize: fontSize.base,
color: color.light,
background: 'transparent',
textTransform: buttonTextTransform,
transition: 'color 750ms ease-out',
border: 'none',
cursor: 'pointer',
'&:hover, &:focus': {
isolate: false,
outline: 0,
color: color.linkHover,
transition: 'color 150ms ease-in',
},
'&:focus:not($isActive)': {
isolate: false,
outline: [[1, 'dotted', color.linkHover]],
},
'& + &': {
isolate: false,
marginLeft: space[1],
},
},
isActive: {
borderBottom: [[2, color.linkHover, 'solid']],
},
});
interface TabButtonProps extends JssInjectedProps {
className?: string;
name: string;
onClick: (e: React.MouseEvent) => void;
active?: boolean;
children: React.ReactNode;
}
export const TabButtonRenderer: React.FunctionComponent = ({
classes,
name,
className,
onClick,
active = false,
children,
}) => {
const classNames = cx(classes.button, className, {
[classes.isActive]: active,
});
return (
{children}
);
};
export default Styled(styles)(TabButtonRenderer);
================================================
FILE: src/client/rsg-components/TabButton/index.ts
================================================
export { default } from 'rsg-components/TabButton/TabButtonRenderer';
================================================
FILE: src/client/rsg-components/Table/Table.spec.tsx
================================================
import React from 'react';
import { createRenderer } from 'react-test-renderer/shallow';
import { TableRenderer, styles } from './TableRenderer';
const columns = [
{
caption: 'Name',
// eslint-disable-next-line react/prop-types
render: ({ name }: { name: string }) => name: {name} ,
},
{
caption: 'Type',
// eslint-disable-next-line react/prop-types
render: ({ type }: { type: string }) => type: {type} ,
},
];
const rows = [
{ name: 'Quattro formaggi', type: 'pizza' },
{ name: 'Tiramisu', type: 'desert' },
{ name: 'Unicorn', type: 'animal' },
];
const props = {
classes: classes(styles),
getRowKey: (row: { name: string }) => row.name,
};
it('should render a table', () => {
const renderer = createRenderer();
renderer.render( );
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
================================================
FILE: src/client/rsg-components/Table/TableRenderer.tsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import Styled, { JssInjectedProps } from 'rsg-components/Styled';
import * as Rsg from '../../../typings';
export const styles = ({ space, color, fontFamily, fontSize }: Rsg.Theme) => ({
table: {
width: '100%',
borderCollapse: 'collapse',
marginBottom: space[4],
},
tableHead: {
borderBottom: [[1, color.border, 'solid']],
},
cellHeading: {
color: color.base,
paddingRight: space[2],
paddingBottom: space[1],
textAlign: 'left',
fontFamily: fontFamily.base,
fontWeight: 'bold',
fontSize: fontSize.small,
whiteSpace: 'nowrap',
},
cell: {
color: color.base,
paddingRight: space[2],
paddingTop: space[1],
paddingBottom: space[1],
verticalAlign: 'top',
fontFamily: fontFamily.base,
fontSize: fontSize.small,
'&:last-child': {
isolate: false,
width: '99%',
paddingRight: 0,
},
'& p:last-child': {
isolate: false,
marginBottom: 0,
},
},
});
interface TableProps extends JssInjectedProps {
columns: {
caption: string;
render(row: any): React.ReactNode;
}[];
rows: any[];
getRowKey(row: any): string;
}
export const TableRenderer: React.FunctionComponent = ({
classes,
columns,
rows,
getRowKey,
}) => {
return (
{columns.map(({ caption }) => (
{caption}
))}
{rows.map(row => (
{columns.map(({ render }, index) => (
{render(row)}
))}
))}
);
};
TableRenderer.propTypes = {
classes: PropTypes.objectOf(PropTypes.string.isRequired).isRequired,
columns: PropTypes.arrayOf(
PropTypes.shape({
caption: PropTypes.string.isRequired,
render: PropTypes.func.isRequired,
}).isRequired
).isRequired,
rows: PropTypes.arrayOf(PropTypes.object).isRequired,
getRowKey: PropTypes.func.isRequired,
};
export default Styled(styles)(TableRenderer);
================================================
FILE: src/client/rsg-components/Table/__snapshots__/Table.spec.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render a table 1`] = `
Name
Type
name:
Quattro formaggi
type:
pizza
name:
Tiramisu
type:
desert
name:
Unicorn
type:
animal
`;
================================================
FILE: src/client/rsg-components/Table/index.ts
================================================
export { default } from 'rsg-components/Table/TableRenderer';
================================================
FILE: src/client/rsg-components/TableOfContents/TableOfContents.spec.tsx
================================================
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { createRenderer } from 'react-test-renderer/shallow';
import TableOfContents from './TableOfContents';
import { TableOfContentsRenderer } from './TableOfContentsRenderer';
import Context from '../Context';
const components = [
{
visibleName: 'Button',
name: 'Button',
href: '#button',
slug: 'button',
},
{
visibleName: 'Input',
name: 'Input',
href: '#input',
slug: 'input',
},
{
visibleName: 'Textarea',
name: 'Textarea',
href: '#textarea',
slug: 'textarea',
},
];
const sections = [
{
visibleName: 'Introduction',
name: 'Introduction',
href: '#introduction',
slug: 'introduction',
content: 'intro.md',
},
{
visibleName: 'Buttons',
name: 'Buttons',
href: '#buttons',
slug: 'buttons',
components: [
{
visibleName: 'Button',
name: 'Button',
href: '#button',
slug: 'button',
},
],
},
{
visibleName: 'Forms',
name: 'Forms',
href: '#forms',
slug: 'forms',
components: [
{
visibleName: 'Input',
name: 'Input',
href: '#input',
slug: 'input',
},
{
visibleName: 'Textarea',
name: 'Textarea',
href: '#textarea',
slug: 'textarea',
},
],
},
];
it('should filter list when search field contains a query', () => {
const searchTerm = 'put';
const { getByPlaceholderText, getAllByTestId, getByTestId } = render(
);
expect(getAllByTestId('rsg-toc-link').length).toBe(3);
fireEvent.change(getByPlaceholderText('Filter by name'), { target: { value: searchTerm } });
expect(getAllByTestId('rsg-toc-link')).toHaveLength(1);
expect(getByTestId('rsg-toc-link')).toHaveTextContent('Input');
});
it('should filter section names', () => {
const searchTerm = 'frm';
const { getByPlaceholderText, getAllByTestId, getByTestId } = render(
);
expect(getAllByTestId('rsg-toc-link').length).toBe(6);
fireEvent.change(getByPlaceholderText('Filter by name'), { target: { value: searchTerm } });
expect(getAllByTestId('rsg-toc-link')).toHaveLength(1);
expect(getByTestId('rsg-toc-link')).toHaveTextContent('Forms');
});
it('should call a callback when input value changed', () => {
const onSearchTermChange = jest.fn();
const searchTerm = 'foo';
const newSearchTerm = 'bar';
const { getByRole } = render(
foo
);
fireEvent.change(getByRole('textbox'), { target: { value: newSearchTerm } });
expect(onSearchTermChange).toBeCalledWith(newSearchTerm);
});
it('should render content of subsections of a section that has no components', () => {
const renderer = createRenderer();
renderer.render(
);
expect(renderer.getRenderOutput()).toMatchInlineSnapshot(`
`);
});
it('should render components of a single top section as root', () => {
const renderer = createRenderer();
renderer.render( );
expect(renderer.getRenderOutput()).toMatchInlineSnapshot(`
`);
});
it('should render as the link will open in a new window only if external presents as true', () => {
const renderer = createRenderer();
renderer.render(
);
expect(renderer.getRenderOutput()).toMatchInlineSnapshot(`
`);
});
/**
* testing this layer with no mocking makes no sense...
*/
it('should render components with useRouterLinks', () => {
const { getAllByRole } = render(
);
expect((getAllByRole('link')[0] as any).href).toMatch(/\/#\/Components$/);
});
/**
* testing this layer with no mocking makes no sense...
* This test should not exist but for good coverage policy this is necessary
*/
it('should detect sections containing current selection when tocMode is collapse', () => {
const context = {
config: {
tocMode: 'collapse',
},
};
const Provider = (props: any) => ;
const { getByText } = render(
);
expect(getByText('1.1')).not.toBeEmptyDOMElement();
});
it('should show sections with expand: true when tocMode is collapse', () => {
const { getByText } = render(
);
expect(getByText('1.1')).toBeVisible();
});
================================================
FILE: src/client/rsg-components/TableOfContents/TableOfContents.tsx
================================================
import React, { Component } from 'react';
import ComponentsList from 'rsg-components/ComponentsList';
import TableOfContentsRenderer from 'rsg-components/TableOfContents/TableOfContentsRenderer';
import filterSectionsByName from '../../utils/filterSectionsByName';
import { getHash } from '../../utils/handleHash';
import * as Rsg from '../../../typings';
interface TableOfContentsProps {
sections: Rsg.Section[];
useRouterLinks?: boolean;
tocMode?: string;
loc?: { hash: string; pathname: string };
}
export default class TableOfContents extends Component {
public state = {
searchTerm: '',
};
private renderLevel(
sections: Rsg.TOCItem[],
useRouterLinks = false,
hashPath: string[] = [],
useHashId = false
): { content: React.ReactElement; containsSelected: boolean } {
// Match selected component in both basic routing and pagePerSection routing.
const { hash, pathname } = this.props.loc ?? window.location;
const windowHash = pathname + (useRouterLinks ? hash : getHash(hash));
let childrenContainSelected = false;
const processedItems = sections.map((section) => {
const children = [...(section.sections || []), ...(section.components || [])];
const sectionDepth = section.sectionDepth || 0;
const childHashPath =
sectionDepth === 0 && useHashId
? hashPath
: [...hashPath, section.name ? section.name : '-'];
const { content, containsSelected } =
children.length > 0
? this.renderLevel(children, useRouterLinks, childHashPath, sectionDepth === 0)
: { content: undefined, containsSelected: false };
const selected =
(!useRouterLinks && section.href ? getHash(section.href) : section.href) === windowHash;
if (containsSelected || selected) {
childrenContainSelected = true;
}
return {
...section,
heading: !!section.name && children.length > 0,
content,
selected,
shouldOpenInNewTab: !!section.external && !!section.externalLink,
initialOpen: this.props.tocMode !== 'collapse' || containsSelected || section.expand,
forcedOpen: !!this.state.searchTerm.length,
};
});
return {
content: ,
containsSelected: childrenContainSelected,
};
}
private renderSections() {
const { searchTerm } = this.state;
const { sections, useRouterLinks } = this.props;
// If there is only one section, we treat it as a root section
// In this case the name of the section won't be rendered and it won't get left padding
// Since a section can contain only other sections,
// we need to make sure not to loose the subsections.
// We will treat those subsections as the new roots.
const firstLevel =
sections.length === 1
? // only use subsections if there actually are subsections
sections[0].sections && sections[0].sections.length
? sections[0].sections
: sections[0].components
: sections;
const filtered = firstLevel
? filterSectionsByName(firstLevel as Rsg.TOCItem[], searchTerm)
: firstLevel || [];
return this.renderLevel(filtered, useRouterLinks).content;
}
public render() {
const handleSearchTermChange = (searchTerm: string) => this.setState({ searchTerm });
return (
{this.renderSections()}
);
}
}
================================================
FILE: src/client/rsg-components/TableOfContents/TableOfContentsRenderer.tsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import { Styles } from 'jss';
import Styled, { JssInjectedProps } from 'rsg-components/Styled';
import * as Rsg from '../../../typings';
const styles = ({ space, color, fontFamily, fontSize, borderRadius }: Rsg.Theme): Styles => ({
root: {
fontFamily: fontFamily.base,
},
search: {
padding: space[2],
},
input: {
display: 'block',
width: '100%',
padding: space[1],
color: color.base,
backgroundColor: color.baseBackground,
fontFamily: fontFamily.base,
fontSize: fontSize.base,
border: [[1, color.border, 'solid']],
borderRadius,
transition: 'all ease-in-out .1s',
'&:focus': {
isolate: false,
borderColor: color.link,
boxShadow: [[0, 0, 0, 2, color.focus]],
outline: 0,
},
'&::placeholder': {
isolate: false,
fontFamily: fontFamily.base,
fontSize: fontSize.base,
color: color.light,
},
},
});
interface TableOfContentsRendererProps extends JssInjectedProps {
children?: React.ReactNode;
searchTerm: string;
onSearchTermChange(term: string): void;
}
export const TableOfContentsRenderer: React.FunctionComponent = ({
classes,
children,
searchTerm,
onSearchTermChange,
}) => {
return (
);
};
TableOfContentsRenderer.propTypes = {
classes: PropTypes.objectOf(PropTypes.string.isRequired).isRequired,
children: PropTypes.any,
searchTerm: PropTypes.string.isRequired,
onSearchTermChange: PropTypes.func.isRequired,
};
export default Styled(styles)(TableOfContentsRenderer);
================================================
FILE: src/client/rsg-components/TableOfContents/index.ts
================================================
export { default } from 'rsg-components/TableOfContents/TableOfContents';
================================================
FILE: src/client/rsg-components/Text/Text.spec.tsx
================================================
import React from 'react';
import { createRenderer } from 'react-test-renderer/shallow';
import { TextRenderer, styles } from './TextRenderer';
const props = {
classes: classes(styles),
};
describe('Text', () => {
it('should render text', () => {
const renderer = createRenderer();
renderer.render(Pizza );
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
it('should render underlined text', () => {
const renderer = createRenderer();
renderer.render(
Pizza
);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
it('should render sized text', () => {
const renderer = createRenderer();
renderer.render(
Pizza
);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
it('should render colored text', () => {
const renderer = createRenderer();
renderer.render(
Pizza
);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
it('should render text with a semantic tag and styles', () => {
const renderer = createRenderer();
renderer.render(
Pizza
);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
it('should render text with a title', () => {
const renderer = createRenderer();
renderer.render(
Pizza
);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
});
================================================
FILE: src/client/rsg-components/Text/TextRenderer.tsx
================================================
import React from 'react';
import cx from 'clsx';
import Styled, { JssInjectedProps } from 'rsg-components/Styled';
import * as Rsg from '../../../typings';
export const styles = ({ fontFamily, fontSize, color }: Rsg.Theme) => ({
text: {
fontFamily: fontFamily.base,
},
inheritSize: {
fontSize: 'inherit',
},
smallSize: {
fontSize: fontSize.small,
},
baseSize: {
fontSize: fontSize.base,
},
textSize: {
fontSize: fontSize.text,
},
baseColor: {
color: color.base,
},
lightColor: {
color: color.light,
},
em: {
fontStyle: 'italic',
},
strong: {
fontWeight: 'bold',
},
isUnderlined: {
borderBottom: [[1, 'dotted', color.lightest]],
},
});
export interface TextProps extends JssInjectedProps {
semantic?: 'em' | 'strong';
size?: 'inherit' | 'small' | 'base' | 'text';
color?: 'base' | 'light';
underlined?: boolean;
children: React.ReactNode;
[intrinsicAttribute: string]: any;
}
export const TextRenderer: React.FunctionComponent = ({
classes,
semantic,
size = 'inherit',
color = 'base',
underlined = false,
children,
...props
}) => {
const Tag = semantic || 'span';
const classNames = cx(classes.text, classes[`${size}Size`], classes[`${color}Color`], {
[classes[Tag]]: !!semantic,
[classes.isUnderlined]: underlined,
});
return (
{children}
);
};
export default Styled(styles)(TextRenderer);
================================================
FILE: src/client/rsg-components/Text/__snapshots__/Text.spec.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Text should render colored text 1`] = `
Pizza
`;
exports[`Text should render sized text 1`] = `
Pizza
`;
exports[`Text should render text 1`] = `
Pizza
`;
exports[`Text should render text with a semantic tag and styles 1`] = `
Pizza
`;
exports[`Text should render text with a title 1`] = `
Pizza
`;
exports[`Text should render underlined text 1`] = `
Pizza
`;
================================================
FILE: src/client/rsg-components/Text/index.ts
================================================
export { default } from 'rsg-components/Text/TextRenderer';
================================================
FILE: src/client/rsg-components/ToolbarButton/ToolbarButton.spec.tsx
================================================
import React from 'react';
import { createRenderer } from 'react-test-renderer/shallow';
import { ToolbarButtonRenderer, styles } from './ToolbarButtonRenderer';
const props = {
classes: classes(styles),
title: 'Pizza button',
};
it('should render a button', () => {
const renderer = createRenderer();
renderer.render(
{}}>
pizza
);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
it('should render a link', () => {
const renderer = createRenderer();
renderer.render(
pizza
);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
it('should pass a class name to a button', () => {
const renderer = createRenderer();
renderer.render(
{}} className="foo-class">
pizza
);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
it('should pass a class name to a link', () => {
const renderer = createRenderer();
renderer.render(
pizza
);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
it('should render a button with small styles', () => {
const renderer = createRenderer();
renderer.render(
{}} small>
butterbrot
);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
================================================
FILE: src/client/rsg-components/ToolbarButton/ToolbarButtonRenderer.tsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import Styled, { JssInjectedProps } from 'rsg-components/Styled';
import cx from 'clsx';
import * as Rsg from '../../../typings';
export const styles = ({ space, color }: Rsg.Theme) => ({
button: {
padding: 2, // Increase clickable area a bit
color: color.light,
background: 'transparent',
transition: 'color 750ms ease-out',
cursor: 'pointer',
'&:hover, &:focus': {
isolate: false,
color: color.linkHover,
transition: 'color 150ms ease-in',
},
'&:focus': {
isolate: false,
outline: [[1, 'dotted', color.linkHover]],
},
'& + &': {
isolate: false,
marginLeft: space[1],
},
// Style react-icons icon passed as children
'& svg': {
width: space[3],
height: space[3],
color: 'currentColor',
cursor: 'inherit',
},
},
isSmall: {
'& svg': {
width: 14,
height: 14,
},
},
});
interface ToolbarButtonProps extends JssInjectedProps {
children: React.ReactNode;
className?: string;
href?: string;
onClick?: () => void;
title?: string;
small?: boolean;
testId?: string;
}
export const ToolbarButtonRenderer: React.FunctionComponent = ({
classes,
className,
onClick,
href,
title,
small,
testId,
children,
}) => {
const classNames = cx(classes.button, className, {
[classes.isSmall]: small,
});
if (href !== undefined) {
return (
{children}
);
}
return (
{children}
);
};
ToolbarButtonRenderer.propTypes = {
classes: PropTypes.objectOf(PropTypes.string.isRequired).isRequired,
className: PropTypes.string,
href: PropTypes.string,
onClick: PropTypes.func,
title: PropTypes.string,
small: PropTypes.bool,
testId: PropTypes.string,
children: PropTypes.any,
};
export default Styled(styles)(ToolbarButtonRenderer);
================================================
FILE: src/client/rsg-components/ToolbarButton/__snapshots__/ToolbarButton.spec.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should pass a class name to a button 1`] = `
pizza
`;
exports[`should pass a class name to a link 1`] = `
pizza
`;
exports[`should render a button 1`] = `
pizza
`;
exports[`should render a button with small styles 1`] = `
butterbrot
`;
exports[`should render a link 1`] = `
pizza
`;
================================================
FILE: src/client/rsg-components/ToolbarButton/index.ts
================================================
export { default } from 'rsg-components/ToolbarButton/ToolbarButtonRenderer';
================================================
FILE: src/client/rsg-components/Tooltip/Tooltip.spec.tsx
================================================
import React from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react';
import Tooltip, { TooltipPlacement } from './TooltipRenderer';
function renderComponent(content = 'tooltip', placement?: TooltipPlacement) {
return render(
);
}
describe('Tooltip', () => {
test('should render child component as is', () => {
const { container, getByTestId } = renderComponent();
expect(container).toContainElement(getByTestId('child'));
});
test('should render content in the tooltop body', () => {
const { container, getByRole } = renderComponent();
fireEvent.focus(getByRole('button'));
expect(container.querySelector('[data-tippy-root]')).toHaveTextContent('tooltip');
});
test('should show the tooltip by focus in', async () => {
const { container, getByRole } = renderComponent();
fireEvent.focus(getByRole('button'));
await waitFor(() =>
expect(container.querySelector('[data-state="visible"]')).toBeInTheDocument()
);
});
test('should show the tooltip by click', async () => {
const { container, getByRole } = renderComponent();
fireEvent.click(getByRole('button'));
await waitFor(() =>
expect(container.querySelector('[data-state="visible"]')).toBeInTheDocument()
);
});
test('should show the tooltip by mouse enter', async () => {
const { container, getByRole } = renderComponent();
fireEvent.mouseEnter(getByRole('button'));
await waitFor(() =>
expect(container.querySelector('[data-state="visible"]')).toBeInTheDocument()
);
});
describe.each([['top'], ['right'], ['left'], ['bottom']])(
'Test placement attribute',
placement => {
test(`should have ${placement} in data-placement attribute`, async () => {
// @ts-ignore
const { container, getByRole } = renderComponent(undefined, placement);
fireEvent.focus(getByRole('button'));
await waitFor(() =>
expect(container.querySelector('[data-state="visible"]')).toBeInTheDocument()
);
expect(container.querySelector('[data-placement]')).toHaveAttribute(
'data-placement',
placement
);
});
}
);
});
================================================
FILE: src/client/rsg-components/Tooltip/TooltipRenderer.tsx
================================================
import React from 'react';
import Tippy from '@tippyjs/react';
import Styled, { JssInjectedProps } from 'rsg-components/Styled';
import * as Rsg from '../../../typings';
export const styles = ({ space, color, borderRadius, fontSize }: Rsg.Theme) => ({
tooltip: {
'&.tippy-box': {
transitionProperty: [['opacity']],
'&[data-state="hidden"]': {
opacity: 0,
},
},
'& .tippy-content': {
padding: space[0],
border: `1px ${color.border} solid`,
borderRadius,
background: color.baseBackground,
boxShadow: [[0, 2, 4, 'rgba(0,0,0,.15)']],
fontSize: fontSize.small,
color: color.type,
},
},
});
export type TooltipPlacement = 'top' | 'right' | 'bottom' | 'left';
export interface TooltipProps extends JssInjectedProps {
children: React.ReactNode;
content: React.ReactNode;
placement?: TooltipPlacement;
}
function TooltipRenderer({ classes, children, content, placement = 'top' }: TooltipProps) {
return (
{children}
);
}
export default Styled(styles)(TooltipRenderer);
================================================
FILE: src/client/rsg-components/Tooltip/index.ts
================================================
export { default } from 'rsg-components/Tooltip/TooltipRenderer';
================================================
FILE: src/client/rsg-components/Type/Type.spec.tsx
================================================
import React from 'react';
import { createRenderer } from 'react-test-renderer/shallow';
import { TypeRenderer, styles } from './TypeRenderer';
const props = {
classes: classes(styles),
};
it('renderer should render type', () => {
const renderer = createRenderer();
renderer.render(Array );
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
================================================
FILE: src/client/rsg-components/Type/TypeRenderer.tsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import Styled, { JssInjectedProps } from 'rsg-components/Styled';
import * as Rsg from '../../../typings';
export const styles = ({ fontFamily, fontSize, color }: Rsg.Theme) => ({
type: {
fontFamily: fontFamily.monospace,
fontSize: fontSize.small,
color: color.type,
},
});
interface TypeProps extends JssInjectedProps {
children: React.ReactNode;
}
export const TypeRenderer: React.FunctionComponent = ({ classes, children }) => {
return {children} ;
};
TypeRenderer.propTypes = {
classes: PropTypes.objectOf(PropTypes.string.isRequired).isRequired,
children: PropTypes.any.isRequired,
};
export default Styled(styles)(TypeRenderer);
================================================
FILE: src/client/rsg-components/Type/__snapshots__/Type.spec.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renderer should render type 1`] = `
Array
`;
================================================
FILE: src/client/rsg-components/Type/index.ts
================================================
export { default } from 'rsg-components/Type/TypeRenderer';
================================================
FILE: src/client/rsg-components/Usage/Usage.spec.tsx
================================================
import React from 'react';
import { createRenderer } from 'react-test-renderer/shallow';
import { PropTypeDescriptor, MethodDescriptor } from 'react-docgen';
import Usage from './Usage';
const props = [
{
name: 'children',
type: { name: 'string' } as PropTypeDescriptor,
required: true,
description: 'Button label.',
},
];
const methods: MethodDescriptor[] = [
{
name: 'set',
params: [
{
name: 'newValue',
description: 'New value for the counter.',
type: { name: 'Number' },
},
],
returns: null,
description: 'Sets the counter to a particular value.',
},
];
describe('Usage', () => {
it('should render props table', () => {
const renderer = createRenderer();
renderer.render( );
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
it('should render methods table', () => {
const renderer = createRenderer();
renderer.render( );
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
it('should render nothing without props and methods', () => {
const renderer = createRenderer();
renderer.render( );
expect(renderer.getRenderOutput()).toBe(null);
});
});
================================================
FILE: src/client/rsg-components/Usage/Usage.tsx
================================================
import React from 'react';
import { MethodDescriptor } from 'react-docgen';
import { PropDescriptor } from 'rsg-components/Props/util';
import Props from 'rsg-components/Props';
import Methods from 'rsg-components/Methods';
import isEmpty from 'lodash/isEmpty';
const Usage: React.FunctionComponent<{
props: { methods?: MethodDescriptor[]; props?: PropDescriptor[] };
}> = ({ props: { props, methods } }) => {
const propsNode = props && !isEmpty(props) && ;
const methodsNode = methods && !isEmpty(methods) && ;
if (!propsNode && !methodsNode) {
return null;
}
return (
{propsNode}
{methodsNode}
);
};
export default Usage;
================================================
FILE: src/client/rsg-components/Usage/__snapshots__/Usage.spec.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Usage should render methods table 1`] = `
`;
exports[`Usage should render props table 1`] = `
`;
================================================
FILE: src/client/rsg-components/Usage/index.ts
================================================
export { default } from 'rsg-components/Usage/Usage';
================================================
FILE: src/client/rsg-components/Version/Version.spec.tsx
================================================
import React from 'react';
import renderer from 'react-test-renderer';
import VersionRenderer from './VersionRenderer';
it('renderer should render version', () => {
const actual = renderer.create(1.2.3-a );
expect(actual.toJSON()).toMatchSnapshot();
});
================================================
FILE: src/client/rsg-components/Version/VersionRenderer.tsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import Styled, { JssInjectedProps } from 'rsg-components/Styled';
import * as Rsg from '../../../typings';
const styles = ({ color, fontFamily, fontSize }: Rsg.Theme) => ({
version: {
color: color.light,
margin: [[5, 0, 0, 0]],
fontFamily: fontFamily.base,
fontSize: fontSize.base,
fontWeight: 'normal',
},
});
interface VersionProps extends JssInjectedProps {
children?: React.ReactNode;
}
export const VersionRenderer: React.FunctionComponent = ({ classes, children }) => {
return (
{children}
);
};
VersionRenderer.propTypes = {
classes: PropTypes.objectOf(PropTypes.string.isRequired).isRequired,
children: PropTypes.any,
};
export default Styled(styles)(VersionRenderer);
================================================
FILE: src/client/rsg-components/Version/__snapshots__/Version.spec.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renderer should render version 1`] = `
1.2.3-a
`;
================================================
FILE: src/client/rsg-components/Version/index.ts
================================================
export { default } from 'rsg-components/Version/VersionRenderer';
================================================
FILE: src/client/rsg-components/Welcome/Welcome.spec.tsx
================================================
import React from 'react';
import { createRenderer } from 'react-test-renderer/shallow';
import { WelcomeRenderer } from './WelcomeRenderer';
it('renderer should render welcome screen', () => {
const renderer = createRenderer();
renderer.render( );
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
================================================
FILE: src/client/rsg-components/Welcome/WelcomeRenderer.tsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import Markdown from 'rsg-components/Markdown';
import Styled, { JssInjectedProps } from 'rsg-components/Styled';
import { DOCS_COMPONENTS } from '../../../scripts/consts';
import * as Rsg from '../../../typings';
const styles = ({ space, maxWidth }: Rsg.Theme) => ({
root: {
maxWidth,
margin: [[0, 'auto']],
padding: space[4],
},
});
interface WelcomeProps extends JssInjectedProps {
patterns: string[];
}
export const WelcomeRenderer: React.FunctionComponent = ({ classes, patterns }) => {
return (
`- \`${p}\``).join('\n')}
Create **styleguide.config.js** file in your project root directory like this:
module.exports = {
components: 'src/components/**/*.js'
};
Read more in the [locating components guide](${DOCS_COMPONENTS}).
`}
/>
);
};
WelcomeRenderer.propTypes = {
classes: PropTypes.objectOf(PropTypes.string.isRequired).isRequired,
patterns: PropTypes.array.isRequired,
};
export default Styled(styles)(WelcomeRenderer);
================================================
FILE: src/client/rsg-components/Welcome/__snapshots__/Welcome.spec.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renderer should render welcome screen 1`] = `
`;
================================================
FILE: src/client/rsg-components/Welcome/index.ts
================================================
export { default } from 'rsg-components/Welcome/WelcomeRenderer';
================================================
FILE: src/client/rsg-components/Wrapper/Wrapper.spec.tsx
================================================
import React from 'react';
import { createRenderer } from 'react-test-renderer/shallow';
import Wrapper from './Wrapper';
it('should render children', () => {
const children = Hello ;
const renderer = createRenderer();
renderer.render( {}}>{children} );
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
it('should call onError handler when React invokes error handler', () => {
const onError = jest.fn();
const renderer = createRenderer();
renderer.render(blah );
// faux error
const err = new Error('err');
const inst = renderer.getMountedInstance() as Wrapper;
if (inst && inst.componentDidCatch) {
inst.componentDidCatch(err);
}
expect(onError).toHaveBeenCalledTimes(1);
expect(onError).toHaveBeenCalledWith(err);
});
================================================
FILE: src/client/rsg-components/Wrapper/Wrapper.ts
================================================
import { Component } from 'react';
import PropTypes from 'prop-types';
interface Props {
onError: (e: Error) => void;
children?: React.ReactNode;
}
export default class Wrapper extends Component {
public static propTypes = {
children: PropTypes.node.isRequired,
onError: PropTypes.func.isRequired,
};
public componentDidCatch(error: Error) {
this.props.onError(error);
}
public render() {
return this.props.children;
}
}
================================================
FILE: src/client/rsg-components/Wrapper/__snapshots__/Wrapper.spec.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should render children 1`] = `
Hello
`;
================================================
FILE: src/client/rsg-components/Wrapper/index.ts
================================================
export { default } from 'rsg-components/Wrapper/Wrapper';
================================================
FILE: src/client/rsg-components/slots/CodeTabButton.tsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import TabButton from 'rsg-components/TabButton';
const CodeTabButton = (props: any) => View Code ;
CodeTabButton.propTypes = {
onClick: PropTypes.func.isRequired,
name: PropTypes.string.isRequired,
active: PropTypes.bool,
};
export default CodeTabButton;
================================================
FILE: src/client/rsg-components/slots/IsolateButton.spec.tsx
================================================
import React from 'react';
import { createRenderer } from 'react-test-renderer/shallow';
import IsolateButton from './IsolateButton';
it('should renderer a link to isolated mode', () => {
const renderer = createRenderer();
renderer.render( );
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
it('should renderer a link to example isolated mode', () => {
const renderer = createRenderer();
renderer.render( );
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
it('should renderer a link home in isolated mode', () => {
const renderer = createRenderer();
renderer.render( );
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
================================================
FILE: src/client/rsg-components/slots/IsolateButton.tsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import { MdFullscreen, MdFullscreenExit } from 'react-icons/md';
import ToolbarButton from 'rsg-components/ToolbarButton';
import getUrl from '../../utils/getUrl';
export interface IsolateButtonProps {
name: string;
example?: number;
isolated?: boolean;
href: string;
}
const IsolateButton = ({ name, example, isolated, href }: IsolateButtonProps) => {
if (isolated && !href) {
return null;
}
const testID = example ? `${name}-${example}-isolate-button` : `${name}-isolate-button`;
return isolated ? (
) : (
);
};
IsolateButton.propTypes = {
name: PropTypes.string.isRequired,
example: PropTypes.number,
isolated: PropTypes.bool,
};
export default IsolateButton;
================================================
FILE: src/client/rsg-components/slots/UsageTabButton.spec.tsx
================================================
import React from 'react';
import { createRenderer } from 'react-test-renderer/shallow';
import UsageTabButton from './UsageTabButton';
const props = {
name: 'Pizza',
onClick: () => {},
};
it('should renderer a button', () => {
const renderer = createRenderer();
renderer.render( );
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
it('should renderer null if there are not props or methods', () => {
const renderer = createRenderer();
renderer.render( );
expect(renderer.getRenderOutput()).toBe(null);
});
================================================
FILE: src/client/rsg-components/slots/UsageTabButton.tsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import TabButton from 'rsg-components/TabButton';
import isEmpty from 'lodash/isEmpty';
export interface UsageTabButtonProps {
name: string;
onClick: (e: React.MouseEvent) => void;
active?: boolean;
props: {
props?: any[];
methods?: any[];
};
}
const UsageTabButton = (props: UsageTabButtonProps) => {
const component = props.props;
const showButton = !isEmpty(component.props) || !isEmpty(component.methods);
return showButton ? Props & methods : null;
};
UsageTabButton.propTypes = {
onClick: PropTypes.func.isRequired,
name: PropTypes.string.isRequired,
props: PropTypes.shape({
props: PropTypes.array,
methods: PropTypes.array,
}).isRequired,
active: PropTypes.bool,
};
export default UsageTabButton;
================================================
FILE: src/client/rsg-components/slots/__snapshots__/IsolateButton.spec.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should renderer a link home in isolated mode 1`] = `
`;
exports[`should renderer a link to example isolated mode 1`] = `
`;
exports[`should renderer a link to isolated mode 1`] = `
`;
================================================
FILE: src/client/rsg-components/slots/__snapshots__/UsageTabButton.spec.tsx.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should renderer a button 1`] = `
Props & methods
`;
================================================
FILE: src/client/rsg-components/slots/index.ts
================================================
import Editor from 'rsg-components/Editor';
import Usage from 'rsg-components/Usage';
import IsolateButton from 'rsg-components/slots/IsolateButton';
import CodeTabButton from 'rsg-components/slots/CodeTabButton';
import UsageTabButton from 'rsg-components/slots/UsageTabButton';
import * as Rsg from '../../../typings';
export const EXAMPLE_TAB_CODE_EDITOR = 'rsg-code-editor';
export const DOCS_TAB_USAGE = 'rsg-usage';
const toolbar = [IsolateButton];
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export default (config?: Rsg.ProcessedStyleguidistConfig) => {
return {
sectionToolbar: toolbar,
componentToolbar: toolbar,
exampleToolbar: toolbar,
exampleTabButtons: [
{
id: EXAMPLE_TAB_CODE_EDITOR,
render: CodeTabButton,
},
],
exampleTabs: [
{
id: EXAMPLE_TAB_CODE_EDITOR,
render: Editor,
},
],
docsTabButtons: [
{
id: DOCS_TAB_USAGE,
render: UsageTabButton,
},
],
docsTabs: [
{
id: DOCS_TAB_USAGE,
render: Usage,
},
],
};
};
================================================
FILE: src/client/styles/__tests__/createStyleSheet.spec.ts
================================================
import * as theme from '../theme';
import createStyleSheet from '../createStyleSheet';
import * as Rsg from '../../../typings';
const customThemeColor = '#123456';
const customThemeBorderColor = '#654321';
const customThemeMaxWidth = 9999;
const customStyleBorderColor = '#ABCDEF';
const customThemeLinkColor = '#CCCAAA';
const testComponentName = 'TestComponentName';
const testRuleName = 'testRule';
const styles = ({ color, borderRadius, maxWidth }: Rsg.Theme) => ({
[testRuleName]: {
color: color.base,
backgroundColor: color.baseBackground,
borderColor: color.border,
borderRadius,
maxWidth,
},
});
const config = {
theme: {
color: {
base: customThemeColor,
border: customThemeBorderColor,
link: customThemeLinkColor,
},
maxWidth: customThemeMaxWidth,
},
styles: {
[testComponentName]: {
[testRuleName]: {
borderColor: customStyleBorderColor,
},
},
},
};
const configWithStylesAsAFunction = {
...config,
styles: (locTheme: Rsg.Theme) => {
return {
[testComponentName]: {
[testRuleName]: {
borderColor: locTheme.color.link,
},
},
};
},
};
describe('createStyleSheet', () => {
it('should use theme variables', () => {
const styleSheet = createStyleSheet(styles, config, testComponentName, '1');
const style = (styleSheet.getRule(testRuleName) as any).style;
expect(style['background-color']).toBe(theme.color.baseBackground);
expect(style['border-radius']).toBe(`${theme.borderRadius}px`);
});
it('should override theme variables with config theme', () => {
const styleSheet = createStyleSheet(styles, config, testComponentName, '2');
const style = (styleSheet.getRule(testRuleName) as any).style;
expect(style.color).toBe(customThemeColor);
expect(style['max-width']).toBe(`${customThemeMaxWidth}px`);
});
it('should override config theme variables with config styles', () => {
const styleSheet = createStyleSheet(styles, config, testComponentName, '3');
const style = (styleSheet.getRule(testRuleName) as any).style;
expect(style['border-color']).toBe(customStyleBorderColor);
});
it('should override config theme variables with config styles as a function', () => {
const styleSheet = createStyleSheet(
styles,
configWithStylesAsAFunction,
testComponentName,
'4'
);
const style = (styleSheet.getRule(testRuleName) as any).style;
expect(style['border-color']).toBe(customThemeLinkColor);
});
});
================================================
FILE: src/client/styles/__tests__/setupjss.spec.ts
================================================
import jssBase from 'jss';
import jss from '../setupjss';
describe('setupjss', () => {
it('should renerate prefixed class names', () => {
const { classes } = jss.createStyleSheet({
root: {},
});
expect(classes.root).toMatch(/^rsg--\w+-\d+$/);
});
it('jss-global plugin should be enabled', () => {
const css = jss
.createStyleSheet({
'@global body': {
color: 'red',
},
})
.toString();
expect(css).toMatch(/^body {/);
});
it('jss plugins should be enabled', () => {
const stylesheet = jss.createStyleSheet({
root: {
backgroundColor: 'tomato',
width: 1,
'&:hover': {
color: 'snow',
},
},
child: {
composes: '$root',
color: 'blue',
},
});
const root = (stylesheet.getRule('root') as any).style;
expect(root).toEqual(expect.any(Object));
expect(root['background-color']).toBe('tomato');
expect(root.width).toBe('1px');
expect(stylesheet.classes.root).toMatch(/^rsg--root-\d+$/);
const child = (stylesheet.getRule('child') as any).style;
expect(child).toEqual(expect.any(Object));
expect(child.color).toBe('blue');
expect(stylesheet.classes.child).toMatch(/^rsg--child-\d+ rsg--root-\d+$/);
const hover = (stylesheet as any).rules.map['.rsg--root-2:hover'];
expect(hover).toEqual(expect.any(Object));
expect(hover.style.color).toBe('snow');
});
it('base jss instance setup shoud not affect Styleguidist styles', () => {
jssBase.setup();
const stylesheet = jss.createStyleSheet({
root: {
width: 1,
},
});
expect(stylesheet.classes.root).toMatch(/^rsg--root-\d+$/);
const root = (stylesheet.getRule('root') as any).style;
expect(root.width).toBe('1px');
});
});
================================================
FILE: src/client/styles/createStyleSheet.ts
================================================
import merge from 'lodash/merge';
import memoize from 'lodash/memoize';
import { Styles, StyleSheet } from 'jss';
import jss from './setupjss';
import * as theme from './theme';
import { RecursivePartial } from '../../typings/RecursivePartial';
import * as Rsg from '../../typings';
/**
* By default lodash/memoize only uses the first argument
* for cache rendering. It works well if the first prameter
* is enough.
* We are Hot Module Replacing (HMR) stylesheets.
* Therefore, we cannot cache stylesheet only by component.
* We need to add cssRevisions to the key fo when the css files update,
* the revision will update and we should update the stylesheet.
*/
export default memoize(
(
styles: (t: Rsg.Theme) => Styles,
config: Rsg.ProcessedStyleguidistCSSConfig,
componentName: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
cssRevision: string
): StyleSheet => {
const mergedTheme = merge, Rsg.Theme, RecursivePartial>(
{},
theme,
config.theme
);
const customStyles =
typeof config.styles === 'function' ? config.styles(mergedTheme) : config.styles;
const mergedStyles: Styles = merge(
{},
styles(mergedTheme),
customStyles && customStyles[componentName]
);
return jss.createStyleSheet(mergedStyles, { meta: componentName, link: true });
},
// calculate the cache key here
(styles, config, componentName, cssRevision) => `${componentName}_${cssRevision}`
);
================================================
FILE: src/client/styles/index.ts
================================================
import './setupjss';
import './styles';
================================================
FILE: src/client/styles/nonInheritedProps.ts
================================================
/* eslint-disable */
/**
* List of non-inheritable properties.
*
* Borrowed from https://github.com/suitcss/preprocessor/blob/master/lib/encapsulation.js
*/
export default {
animation: 'none 0s ease 0s 1 normal none running',
'backface-visibility': 'visible',
background: 'transparent none repeat 0 0 / auto auto padding-box border-box scroll',
border: 'medium none currentColor',
'border-image': 'none',
'border-radius': '0',
bottom: 'auto',
'box-shadow': 'none',
clear: 'none',
clip: 'auto',
columns: 'auto',
'column-count': 'auto',
'column-fill': 'balance',
'column-gap': 'normal',
'column-rule': 'medium none currentColor',
'column-span': '1',
'column-width': 'auto',
content: 'normal',
'counter-increment': 'none',
'counter-reset': 'none',
float: 'none',
height: 'auto',
hyphens: 'none',
left: 'auto',
margin: '0',
'max-height': 'none',
'max-width': 'none',
'min-height': '0',
'min-width': '0',
opacity: '1',
outline: 'medium none invert',
overflow: 'visible',
'overflow-x': 'visible',
'overflow-y': 'visible',
padding: '0',
'page-break-after': 'auto',
'page-break-before': 'auto',
'page-break-inside': 'auto',
perspective: 'none',
'perspective-origin': '50% 50%',
position: 'static',
right: 'auto',
'table-layout': 'auto',
'text-decoration': 'none',
top: 'auto',
transform: 'none',
'transform-origin': '50% 50% 0',
'transform-style': 'flat',
transition: 'none 0s ease 0s',
'unicode-bidi': 'normal',
'vertical-align': 'baseline',
width: 'auto',
'z-index': 'auto',
};
================================================
FILE: src/client/styles/prismTheme.ts
================================================
import * as Rsg from '../../typings';
const prismTheme = ({ color }: Pick) => ({
'&': {
color: color.codeBase,
},
[`& .token.comment,
& .token.prolog,
& .token.doctype,
& .token.cdata`]: {
isolate: false,
color: color.codeComment,
},
[`& .token.punctuation`]: {
isolate: false,
color: color.codePunctuation,
},
[`& .namespace`]: {
isolate: false,
opacity: 0.7,
},
[`& .token.property,
& .token.tag,
& .token.boolean,
& .token.number,
& .token.constant,
& .token.symbol`]: {
isolate: false,
color: color.codeProperty,
},
[`& .token.deleted`]: {
isolate: false,
color: color.codeDeleted,
},
[`& .token.selector,
& .token.attr-name,
& .token.string,
& .token.char,
& .token.builtin`]: {
isolate: false,
color: color.codeString,
},
[`& .token.inserted`]: {
isolate: false,
color: color.codeInserted,
},
[`& .token.operator,
& .token.entity,
& .token.url,
& .language-css .token.string,
& .style .token.string`]: {
isolate: false,
color: color.codeOperator,
},
[`& .token.atrule,
& .token.attr-value,
& .token.keyword`]: {
isolate: false,
color: color.codeKeyword,
},
[`& .token.function,
& .token.class-name`]: {
isolate: false,
color: color.codeFunction,
},
[`& .token.regex,
& .token.important,
& .token.variable`]: {
isolate: false,
color: color.codeVariable,
},
[`& .token.important,
& .token.bold`]: {
isolate: false,
fontWeight: 'bold',
},
[`& .token.italic`]: {
isolate: false,
fontStyle: 'italic',
},
[`& .token.entity`]: {
isolate: false,
cursor: 'help',
},
});
export default prismTheme;
================================================
FILE: src/client/styles/setupjss.ts
================================================
import { create } from 'jss';
import global from 'jss-plugin-global';
import isolate from 'jss-plugin-isolate';
import nested from 'jss-plugin-nested';
import camelCase from 'jss-plugin-camel-case';
import defaultUnit from 'jss-plugin-default-unit';
import compose from 'jss-plugin-compose';
import nonInheritedProps from './nonInheritedProps';
const createGenerateId = () => {
let counter = 0;
return (rule: { key: string }) => `rsg--${rule.key}-${counter++}`;
};
const jss = create({
createGenerateId,
plugins: [
global(),
isolate({
reset: {
// Reset all inherited and non-inherited properties
...nonInheritedProps,
// “Global” styles for all components
boxSizing: 'border-box',
// Allow inheritance because it may be set on body and should be available for user components
color: 'inherit',
font: 'inherit',
fontFamily: 'inherit',
fontSize: 'inherit',
fontWeight: 'inherit',
lineHeight: 'inherit',
},
}),
nested(),
camelCase(),
defaultUnit(),
compose(),
],
});
export default jss;
================================================
FILE: src/client/styles/styles.ts
================================================
import jss from './setupjss';
const styles = {
// Global styles
body: {
isolate: false,
margin: 0,
padding: 0,
minWidth: 0,
maxWidth: '100%',
border: 0,
},
};
// Attach styles to body
const { body } = jss.createStyleSheet(styles).attach().classes;
document.body.classList.add(body);
================================================
FILE: src/client/styles/theme.ts
================================================
export const spaceFactor = 8;
export const space = [
spaceFactor / 2, // 4
spaceFactor, // 8
spaceFactor * 2, // 16
spaceFactor * 3, // 24
spaceFactor * 4, // 32
spaceFactor * 5, // 40
spaceFactor * 6, // 48
];
export const color = {
base: '#333',
light: '#767676',
lightest: '#ccc',
link: '#1673b1',
linkHover: '#e90',
focus: 'rgba(22, 115, 177, 0.25)',
border: '#e8e8e8',
name: '#690',
type: '#905',
error: '#c00',
baseBackground: '#fff',
codeBackground: '#f5f5f5',
sidebarBackground: '#f5f5f5',
ribbonBackground: '#e90',
ribbonText: '#fff',
// Based on default Prism theme
codeBase: '#333',
codeComment: '#6d6d6d',
codePunctuation: '#999',
codeProperty: '#905',
codeDeleted: '#905',
codeString: '#690',
codeInserted: '#690',
codeOperator: '#9a6e3a',
codeKeyword: '#1673b1',
codeFunction: '#DD4A68',
codeVariable: '#e90',
};
export const fontFamily = {
base: [
'-apple-system',
'BlinkMacSystemFont',
'"Segoe UI"',
'"Roboto"',
'"Oxygen"',
'"Ubuntu"',
'"Cantarell"',
'"Fira Sans"',
'"Droid Sans"',
'"Helvetica Neue"',
'sans-serif',
],
monospace: ['Consolas', '"Liberation Mono"', 'Menlo', 'monospace'],
};
export const fontSize = {
base: 15,
text: 16,
small: 13,
h1: 48,
h2: 36,
h3: 24,
h4: 18,
h5: 16,
h6: 16,
};
export const mq = {
small: '@media (max-width: 600px)',
};
export const borderRadius = 3;
export const maxWidth = 1000;
export const sidebarWidth = 200;
export const buttonTextTransform = 'uppercase';
================================================
FILE: src/client/utils/__tests__/__snapshots__/filterSectionsByName.spec.ts.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`filterSectionsByName should recursively filter sections and components by name 1`] = `
Array [
Object {
"components": Array [],
"name": "General",
"sections": Array [
Object {
"components": Array [
Object {
"name": "Button",
},
],
"name": "Particles",
"sections": Array [],
},
],
},
]
`;
exports[`filterSectionsByName should skip sections without matches inside 1`] = `
Array [
Object {
"components": Array [],
"name": "General",
"sections": Array [],
},
]
`;
================================================
FILE: src/client/utils/__tests__/__snapshots__/getAst.spec.ts.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`getAst return AST 1`] = `
Node {
"body": Array [
Node {
"end": 2,
"expression": Node {
"end": 2,
"raw": "42",
"start": 0,
"type": "Literal",
"value": 42,
},
"start": 0,
"type": "ExpressionStatement",
},
],
"end": 2,
"sourceType": "module",
"start": 0,
"type": "Program",
}
`;
================================================
FILE: src/client/utils/__tests__/__snapshots__/getRouteData.spec.ts.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`getRouteData should return first section if pagePerSection and hash is empty 1`] = `
Object {
"displayMode": "all",
"sections": Array [
Object {
"components": Array [
Object {
"module": 1,
"name": "Button",
"props": Object {
"displayName": "Button",
"examples": Array [
Object {
"content": "const a = 0",
"evalInContext": [Function],
"type": "code",
},
Object {
"content": "# title",
"type": "markdown",
},
],
},
},
Object {
"module": 2,
"name": "Image",
"props": Object {
"displayName": "Image",
},
},
],
"exampleMode": "collapse",
"name": "Components",
"sectionDepth": 0,
"sections": Array [],
"slug": "components",
"usageMode": "collapse",
},
],
}
`;
exports[`getRouteData should return not found if pagePerSection and hash is #/Buttons/Label/Not 1`] = `
Object {
"displayMode": "notFound",
"sections": Array [],
}
`;
exports[`getRouteData should return one component 1`] = `
Object {
"displayMode": "component",
"sections": Array [
Object {
"components": Array [
Object {
"module": 1,
"name": "Button",
"props": Object {
"displayName": "Button",
"examples": Array [
Object {
"content": "const a = 0",
"evalInContext": [Function],
"type": "code",
},
Object {
"content": "# title",
"type": "markdown",
},
],
},
},
],
"exampleMode": "collapse",
"slug": "components",
"usageMode": "collapse",
},
],
}
`;
exports[`getRouteData should return one component if pagePerSection and hash is #/Buttons/Label 1`] = `
Object {
"displayMode": "all",
"sections": Array [
Object {
"components": Array [
Object {
"module": 1,
"name": "Label",
},
],
"exampleMode": "collapse",
"slug": "buttons",
"usageMode": "collapse",
},
],
}
`;
exports[`getRouteData should return one example from a component 1`] = `
Object {
"displayMode": "example",
"sections": Array [
Object {
"components": Array [
Object {
"module": 1,
"name": "Button",
"props": Object {
"displayName": "Button",
"examples": Array [
Object {
"content": "# title",
"type": "markdown",
},
],
},
},
],
"exampleMode": "collapse",
"slug": "components",
"usageMode": "collapse",
},
],
}
`;
exports[`getRouteData should return one example from a section 1`] = `
Object {
"displayMode": "example",
"sections": Array [
Object {
"components": Array [],
"content": Array [
Object {
"content": "# title",
"type": "markdown",
},
],
"exampleMode": "collapse",
"name": "Section",
"sectionDepth": 0,
"sections": Array [],
"slug": "section",
"usageMode": "collapse",
},
],
}
`;
exports[`getRouteData should return one section 1`] = `
Object {
"displayMode": "section",
"sections": Array [
Object {
"components": Array [],
"content": Array [
Object {
"content": "const a = 0",
"evalInContext": [Function],
"type": "code",
},
Object {
"content": "# title",
"type": "markdown",
},
],
"exampleMode": "collapse",
"name": "Section",
"sectionDepth": 0,
"sections": Array [],
"slug": "section",
"usageMode": "collapse",
},
],
}
`;
exports[`getRouteData should return one section if pagePerSection and hash is #/Section 1`] = `
Object {
"displayMode": "all",
"sections": Array [
Object {
"components": Array [],
"content": Array [
Object {
"content": "const a = 0",
"evalInContext": [Function],
"type": "code",
},
Object {
"content": "# title",
"type": "markdown",
},
],
"exampleMode": "collapse",
"name": "Section",
"sectionDepth": 0,
"sections": Array [],
"slug": "section",
"usageMode": "collapse",
},
],
}
`;
exports[`getRouteData should return one section without components if pagePerSection and hash is #/Buttons 1`] = `
Object {
"displayMode": "all",
"sections": Array [
Object {
"components": Array [],
"exampleMode": "collapse",
"name": "Buttons",
"sectionDepth": 2,
"sections": Array [],
"slug": "buttons",
"usageMode": "collapse",
},
],
}
`;
================================================
FILE: src/client/utils/__tests__/compileCode.spec.ts
================================================
import compileCode from '../compileCode';
import config from '../../../scripts/schemas/config';
const compilerConfig = config.compilerConfig.default;
describe('compileCode', () => {
test('compile ES6 to ES5', () => {
const result = compileCode(`const {foo, bar} = baz`, compilerConfig);
expect(result).toMatchInlineSnapshot(`
"var foo = baz.foo;
var bar = baz.bar;"
`);
});
test('transform imports to require()', () => {
const result = compileCode(`import foo from 'bar'`, compilerConfig);
expect(result).toMatchInlineSnapshot(`
"const bar$0 = require('bar');
const foo = bar$0.default || bar$0;"
`);
});
test('transform async/await is not throw an error', () => {
const onError = jest.fn();
const result = compileCode(
`async function asyncFunction() { return await Promise.resolve(); }`,
compilerConfig,
onError
);
expect(onError).not.toHaveBeenCalled();
expect(result).toMatchInlineSnapshot(
`"async function asyncFunction() { return await Promise.resolve(); }"`
);
});
test('transform imports to require() in front of JSX', () => {
const result = compileCode(
`
import foo from 'bar';
import Button from 'button';
`,
compilerConfig
);
expect(result).toMatchInlineSnapshot(`
"
const bar$0 = require('bar');
const foo = bar$0.default || bar$0;
const button$0 = require('button');
const Button = button$0.default || button$0;
React.createElement( Button, null )"
`);
});
test('wrap JSX in Fragment if adjacent on line 1', () => {
const result = compileCode(` `, compilerConfig);
expect(result).toMatchInlineSnapshot(
`"React.createElement( React.Fragment, null, React.createElement( 'span', null ), React.createElement( 'span', null ) );"`
);
});
test('don’t wrap JSX in Fragment if there is only one statement', () => {
const result = compileCode(` ;`, compilerConfig);
expect(result).toMatchInlineSnapshot(`"React.createElement( Button, null );"`);
});
test('don’t wrap JSX in Fragment if it’s in the middle', () => {
const result = compileCode(
`const {foo, bar} = baz;
Click
`,
compilerConfig
);
expect(result).toMatchInlineSnapshot(`
"var foo = baz.foo;
var bar = baz.bar;
React.createElement( 'div', null,
React.createElement( 'button', null, \\"Click\\" )
)"
`);
});
test('tagged template literals', () => {
const result = compileCode(
`const Button = styled.button\`
color: tomato;
\`;
`,
compilerConfig
);
expect(result).toMatchInlineSnapshot(`
"var templateObject = Object.freeze([\\"\\\\n\\\\tcolor: tomato;\\\\n\\"]);
var Button = styled.button(templateObject);
React.createElement( Button, null )
"
`);
});
test('onError callback', () => {
const onError = jest.fn();
const result = compileCode(`=`, compilerConfig, onError);
expect(result).toBe('');
expect(onError).toHaveBeenCalledWith(
expect.objectContaining({ message: 'Unexpected token (1:0)' })
);
});
});
================================================
FILE: src/client/utils/__tests__/filterComponentExamples.spec.ts
================================================
import deepfreeze from 'deepfreeze';
import filterComponentExamples from '../filterComponentExamples';
import * as Rsg from '../../../typings';
const examples: Rsg.Example[] = ['a', 'b', 'c', 'd'].map(x => ({ type: 'markdown', content: x }));
const component = deepfreeze({
props: {
examples,
},
other: 'info',
});
describe('filterComponentExamples', () => {
it('should return a shallow copy of a component with example filtered by given index', () => {
const result = filterComponentExamples(component, 2);
expect(result).toEqual({
props: {
examples: [{ type: 'markdown', content: 'c' }],
},
other: 'info',
});
});
});
================================================
FILE: src/client/utils/__tests__/filterComponentsByExactName.spec.ts
================================================
import deepfreeze from 'deepfreeze';
import filterComponentsByExactName from '../filterComponentsByExactName';
const components = deepfreeze([
{
name: 'Button',
},
{
name: 'Image',
},
]);
describe('filterComponentsByExactName', () => {
it('should return components with exact name', () => {
const result = filterComponentsByExactName(components, 'Image');
expect(result.map(x => x.name)).toEqual(['Image']);
});
});
================================================
FILE: src/client/utils/__tests__/filterComponentsByName.spec.ts
================================================
import deepfreeze from 'deepfreeze';
import filterComponentsByName from '../filterComponentsByName';
const components = deepfreeze([
{
name: 'Button',
},
{
name: 'Image',
},
{
name: 'Input',
},
{
name: 'Link',
},
{
name: 'Textarea',
},
]);
describe('filterComponentsByName', () => {
it('should return initial list with empty query', () => {
const result = filterComponentsByName(components, '');
expect(result).toEqual(components);
});
it('should return filtered list, should ignore case', () => {
const result = filterComponentsByName(components, 'button');
expect(result).toEqual([{ name: 'Button' }]);
});
it('should return empty list when nothing found', () => {
const result = filterComponentsByName(components, 'pizza');
expect(result).toEqual([]);
});
it('should return all components if all of them match query', () => {
// It doesn’t happen when RegExp has global flag for some reason
const components2 = [
{ name: 'Button' },
{ name: 'CounterButton' },
{ name: 'PushButton' },
{ name: 'RandomButtom' },
{ name: 'WrappedButton' },
];
const result = filterComponentsByName(components2, 'bu');
expect(result).toEqual(components2);
});
});
================================================
FILE: src/client/utils/__tests__/filterComponentsInSectionsByExactName.spec.ts
================================================
import deepfreeze from 'deepfreeze';
import filterComponentsInSectionsByExactName from '../filterComponentsInSectionsByExactName';
const sections = deepfreeze([
{
name: 'General',
sections: [
{
name: 'Particles',
components: [
{
name: 'Button',
},
{
name: 'Image',
},
],
},
],
},
]);
describe('filterComponentsInSectionsByExactName', () => {
it('should return components at any level with exact name', () => {
const result = filterComponentsInSectionsByExactName(sections, 'Image', true)[0];
expect(result.components && result.components.map(x => x.name)).toEqual(['Image']);
});
});
================================================
FILE: src/client/utils/__tests__/filterSectionExamples.spec.ts
================================================
import deepfreeze from 'deepfreeze';
import filterSectionExamples from '../filterSectionExamples';
const section = deepfreeze({
content: ['a', 'b', 'c', 'd'],
other: 'info',
});
describe('filterSectionExamples', () => {
it('should return a shallow copy of a section with example filtered by given index', () => {
const result = filterSectionExamples(section as any, 2);
expect(result).toEqual({
content: ['c'],
other: 'info',
});
});
});
================================================
FILE: src/client/utils/__tests__/filterSectionsByName.spec.ts
================================================
import deepfreeze from 'deepfreeze';
import filterSectionsByName from '../filterSectionsByName';
const sections = deepfreeze([
{
name: 'General',
sections: [
{
name: 'Particles',
components: [
{
name: 'Button',
},
{
name: 'Image',
},
],
},
],
},
]);
describe('filterSectionsByName', () => {
it('should recursively filter sections and components by name', () => {
const result = filterSectionsByName(sections, 'button');
expect(result).toMatchSnapshot();
});
it('should skip sections without matches inside', () => {
const result = filterSectionsByName(sections, 'general');
expect(result).toMatchSnapshot();
});
it('should return empty array if no components of sections match query', () => {
const result = filterSectionsByName(sections, 'pizza');
expect(result).toEqual([]);
});
});
================================================
FILE: src/client/utils/__tests__/findSection.spec.ts
================================================
import findSection from '../findSection';
const sections = [
{
name: 'General',
sections: [
{
name: 'Particles',
components: [
{
name: 'Button',
},
{
name: 'Image',
},
],
},
],
},
];
describe('findSection', () => {
it('should return top level section', () => {
const result = findSection(sections, 'General');
expect(result).toEqual(sections[0]);
});
it('should return nested sections', () => {
const result = findSection(sections, 'Particles');
expect(result).toEqual(sections[0].sections[0]);
});
it('should return undefined when no sections found', () => {
const result = findSection(sections, 'Pizza');
expect(result).toBeFalsy();
});
});
================================================
FILE: src/client/utils/__tests__/getAst.spec.ts
================================================
import getAst from '../getAst';
describe('getAst', () => {
test('return AST', () => {
const result = getAst(`42`);
expect(result).toMatchSnapshot();
});
});
================================================
FILE: src/client/utils/__tests__/getComponent.spec.ts
================================================
import getComponent from '../getComponent';
describe('getComponent', () => {
describe('if there is a default export in the module', () => {
it('should return that', () => {
const module = { default: 'useMe' };
const actual = getComponent(module);
expect(actual).toBe(module.default);
});
});
describe('if it is a CommonJS module and exports a function', () => {
it('should return the module', () => {
const testCases = [() => {}, function() {}, class Class {}];
testCases.forEach(testCase => {
const actual = getComponent(testCase);
expect(actual).toBe(testCase);
});
});
});
describe('if there is only one named export in the module', () => {
it('should return that', () => {
const module = { oneNamedExport: 'isLonely' };
const actual = getComponent(module);
expect(actual).toBe(module.oneNamedExport);
});
});
describe('if there is a named export whose name matches the name argument', () => {
it('should return that', () => {
const name = 'Component';
const module = { [name]: 'isNamed', OtherComponent: 'isAlsoNamed' };
const actual = getComponent(module, name);
expect(actual).toBe(module[name]);
});
});
describe('if there is more than one named export and no matching name', () => {
it('should fall back on returning the module as a whole', () => {
const name = 'Component';
const module = { RandomName: 'isNamed', confusingExport: 123 };
const actual = getComponent(module, name);
expect(actual).toBe(module);
});
});
});
================================================
FILE: src/client/utils/__tests__/getFilterRegExp.spec.ts
================================================
import getFilterRegExp from '../getFilterRegExp';
describe('getFilterRegExp', () => {
it('should return a RegExp', () => {
const result = getFilterRegExp('');
expect(result instanceof RegExp).toBe(true);
});
it('RegExp should fuzzy match a string', () => {
const result = getFilterRegExp('btn');
expect('button').toMatch(result);
});
it('RegExp should not match when string is different', () => {
const result = getFilterRegExp('buttons');
expect('button').not.toMatch(result);
});
it('should not throw when query contains special characters', () => {
const fn = () => getFilterRegExp('\\');
expect(fn).not.toThrow();
});
it('RegExp should ignore non-alphanumeric characters', () => {
const result = getFilterRegExp('#$b()tn');
expect('button').toMatch(result);
});
});
================================================
FILE: src/client/utils/__tests__/getInfoFromHash.spec.ts
================================================
import getInfoFromHash from '../getInfoFromHash';
describe('getInfoFromHash', () => {
it('should return important part of hash if it contains component name', () => {
const result = getInfoFromHash('#!/Button');
expect(result).toEqual({
isolate: true,
hashArray: ['Button'],
targetName: 'Button',
targetIndex: undefined,
});
});
it('should return an empty object if hash contains no component name', () => {
const result = getInfoFromHash('Button');
expect(result).toEqual({});
});
it('should return the decoded targetName when router name is not English such as Chinese', () => {
const result = getInfoFromHash('#!/Api%20%E7%BB%84%E4%BB%B6');
expect(result).toEqual({
isolate: true,
hashArray: ['Api 组件'],
targetName: 'Api 组件',
targetIndex: undefined,
});
});
it('hashArray should return an array of each deep and isolate should return false when the url starts with #/', () => {
const result = getInfoFromHash('#/Documentation/Files/Buttons');
expect(result).toEqual({
isolate: false,
hashArray: ['Documentation', 'Files', 'Buttons'],
targetName: 'Documentation',
targetIndex: undefined,
});
});
it('should extract target index when the URL ends with a number', () => {
const result = getInfoFromHash('#/Documentation/Files/Buttons/5');
expect(result).toEqual({
isolate: false,
hashArray: ['Documentation', 'Files', 'Buttons'],
targetName: 'Documentation',
targetIndex: 5,
});
});
it('should return a proper parsed result even though the hash starts with a number', () => {
const result = getInfoFromHash('#/1.Documentation');
expect(result).toEqual({
isolate: false,
hashArray: ['1.Documentation'],
targetName: '1.Documentation',
targetIndex: undefined,
});
});
});
================================================
FILE: src/client/utils/__tests__/getPageTitle.spec.ts
================================================
import getPageTitle from '../getPageTitle';
const baseTitle = 'Styleguide';
describe('getPageTitle', () => {
it('should return style guide title for the all view', () => {
const result = getPageTitle([], baseTitle, 'all');
expect(result).toBe(baseTitle);
});
it('should return component name for component isolation mode', () => {
const name = 'Component';
const result = getPageTitle([{ components: [{ name }] }], baseTitle, 'component');
expect(result).toMatch(name);
});
it('should return component name for example isolation mode', () => {
const name = 'Component';
const result = getPageTitle([{ components: [{ name }] }], baseTitle, 'example');
expect(result).toMatch(name);
});
it('should return section name for example isolation mode of a example content', () => {
const sectionName = 'Section';
const result = getPageTitle(
[{ name: sectionName, content: [], components: [] }],
baseTitle,
'example'
);
expect(result).toMatch(sectionName);
});
it('should return section name for example isolation mode, if no components are passed', () => {
const name = 'Section';
const result = getPageTitle([{ name }], baseTitle, 'example');
expect(result).toMatch(name);
});
it('should return section name for section isolation mode', () => {
const name = 'Section';
const result = getPageTitle([{ name }], baseTitle, 'section');
expect(result).toMatch(name);
});
it('should return Error 404 for notFound isolation mode', () => {
const name = 'Section';
const result = getPageTitle([{ name }], baseTitle, 'notFound');
expect(result).toMatch('Page not found');
});
});
================================================
FILE: src/client/utils/__tests__/getRouteData.spec.ts
================================================
import deepfreeze from 'deepfreeze';
import getRouteData from '../getRouteData';
import { DisplayModes } from '../../consts';
import * as Rsg from '../../../typings';
const example0: Rsg.Example = {
type: 'code',
content: 'const a = 0',
evalInContext: () => () => 3,
};
const example1: Rsg.Example = {
type: 'markdown',
content: '# title',
};
const sections: Rsg.Section[] = deepfreeze([
{
sections: [
{
name: 'Components',
slug: 'components',
components: [
{
name: 'Button',
props: {
displayName: 'Button',
examples: [example0, example1],
},
module: 1,
},
{
name: 'Image',
props: {
displayName: 'Image',
},
module: 2,
},
],
sections: [],
exampleMode: 'collapse',
usageMode: 'collapse',
sectionDepth: 0,
},
{
name: 'Section',
slug: 'section',
content: [example0, example1],
components: [],
sections: [],
exampleMode: 'collapse',
usageMode: 'collapse',
sectionDepth: 0,
},
{
name: 'Buttons',
slug: 'buttons',
components: [
{
name: 'Label',
module: 1,
},
{
name: 'RandomButton',
module: 2,
},
],
sections: [],
exampleMode: 'collapse',
usageMode: 'collapse',
sectionDepth: 2,
},
],
},
]);
describe('getRouteData', () => {
it('should return "all" mode by default', () => {
const result = getRouteData([], '');
expect(result.displayMode).toBe(DisplayModes.all);
});
it('should return one component', () => {
const result = getRouteData(sections, '#!/Button');
expect(result).toMatchSnapshot();
});
it('should return one section', () => {
const result = getRouteData(sections, '#!/Section');
expect(result).toMatchSnapshot();
});
it('should return one example from a component', () => {
const result = getRouteData(sections, '#!/Button/1');
expect(result).toMatchSnapshot();
});
it('should return one example from a section', () => {
const result = getRouteData(sections, '#!/Section/1');
expect(result).toMatchSnapshot();
});
it('should return first section if pagePerSection and hash is empty', () => {
const subSection = sections[0].sections as Rsg.Section[];
const result = getRouteData(subSection, '', true);
expect(result).toMatchSnapshot();
});
it('should return one section if pagePerSection and hash is #/Section', () => {
const result = getRouteData(sections, '#/Section', true);
expect(result).toMatchSnapshot();
});
it('should return one section without components if pagePerSection and hash is #/Buttons', () => {
const result = getRouteData(sections, '#/Buttons', true);
expect(result).toMatchSnapshot();
});
it('should return one component if pagePerSection and hash is #/Buttons/Label', () => {
const result = getRouteData(sections, '#/Buttons/Label', true);
expect(result).toMatchSnapshot();
});
it('should return not found if pagePerSection and hash is #/Buttons/Label/Not', () => {
const result = getRouteData(sections, '#/Buttons/Label/Not', true);
expect(result).toMatchSnapshot();
});
});
================================================
FILE: src/client/utils/__tests__/getUrl.spec.ts
================================================
import getUrl from '../getUrl';
describe('getUrl', () => {
const loc = {
origin: 'http://example.com',
pathname: '/styleguide/',
hash: '#/Components',
};
const locHashedURL = {
origin: 'http://example.com',
pathname: '/styleguide/',
hash: '#button',
};
const name = 'FooBar';
const slug = 'foobar';
it('should return a home URL', () => {
const result = getUrl({}, loc);
expect(result).toBe('/styleguide/');
});
it('should return an absolute home URL', () => {
const result = getUrl({ absolute: true }, loc);
expect(result).toBe('http://example.com/styleguide/');
});
it('should return an anchor URL', () => {
const result = getUrl({ name, slug, anchor: true }, loc);
expect(result).toBe('/styleguide/#foobar');
});
it('should return an absolute anchor URL', () => {
const result = getUrl({ name, slug, anchor: true, absolute: true }, loc);
expect(result).toBe('http://example.com/styleguide/#foobar');
});
it('should return an isolated URL', () => {
const result = getUrl({ name, slug, isolated: true }, loc);
expect(result).toBe('/styleguide/#!/Components/FooBar');
});
it('should return an absolute isolated URL', () => {
const result = getUrl({ name, slug, isolated: true, absolute: true }, loc);
expect(result).toBe('http://example.com/styleguide/#!/Components/FooBar');
});
it('should return an isolated example URL', () => {
const result = getUrl({ name, slug, example: 3, isolated: true }, loc);
expect(result).toBe('/styleguide/#!/Components/FooBar/3');
});
it('should return an isolated example for a HashedURL', () => {
const result = getUrl({ name, slug, example: 0, isolated: true }, locHashedURL);
expect(result).toBe('/styleguide/#!/FooBar/0');
});
it('should return an isolated example=0 URL', () => {
const result = getUrl({ name, slug, example: 0, isolated: true }, loc);
expect(result).toBe('/styleguide/#!/Components/FooBar/0');
});
it('should return an absolute isolated example URL', () => {
const result = getUrl({ name, slug, example: 3, isolated: true, absolute: true }, loc);
expect(result).toBe('http://example.com/styleguide/#!/Components/FooBar/3');
});
it('should return a nochrome URL', () => {
const result = getUrl({ name, slug, nochrome: true }, loc);
expect(result).toBe('/styleguide/?nochrome#!/Components/FooBar');
});
it('should return an absolute nochrome URL', () => {
const result = getUrl({ name, slug, nochrome: true, absolute: true }, loc);
expect(result).toBe('http://example.com/styleguide/?nochrome#!/Components/FooBar');
});
it('should return a route path', () => {
const result = getUrl({ name, slug, hashPath: ['Documentation'] }, loc);
expect(result).toBe('/styleguide/#/Documentation/FooBar');
});
it('should return a route path with encoded name if name has inappropriate symbols', () => {
const result = getUrl({ name: '@foo/components', slug, hashPath: ['Documentation'] }, loc);
expect(result).toBe('/styleguide/#/Documentation/%40foo%2Fcomponents');
});
it('should return a route path with encoded name if sections (hashPath) has inappropriate symbols', () => {
expect(
getUrl({ name: '@foo/components', slug, hashPath: ['@foo/bar-documentation'] }, loc)
).toBe('/styleguide/#/%40foo%2Fbar-documentation/%40foo%2Fcomponents');
expect(
getUrl(
{
name: '@foo/components',
slug,
hashPath: ['@foo/bar-documentation', '@foo/bar-activations-section'],
},
loc
)
).toBe(
'/styleguide/#/%40foo%2Fbar-documentation/%40foo%2Fbar-activations-section/%40foo%2Fcomponents'
);
});
it('should return a route path with a param id=foobar', () => {
const result = getUrl({ name, slug, hashPath: ['Documentation'], useSlugAsIdParam: true }, loc);
expect(result).toBe('/styleguide/#/Documentation?id=foobar');
});
it('should return a param id=foobar', () => {
const result = getUrl({ name, slug, takeHash: true, useSlugAsIdParam: true }, loc);
expect(result).toBe('/styleguide/#/Components?id=foobar');
});
it('should return to param id = foobar even if the hash has parameters', () => {
const result = getUrl(
{ name, slug, takeHash: true, useSlugAsIdParam: true },
{
...loc,
hash: '#/Components?foo=foobar',
}
);
expect(result).toBe('/styleguide/#/Components?id=foobar');
});
});
================================================
FILE: src/client/utils/__tests__/handleHash.spec.ts
================================================
import { hasInHash, getHash, getHashAsArray, getParameterByName } from '../handleHash';
describe('handleHash', () => {
const isolateHash = '#!/';
const routeHash = '#/';
it('hasInHash should return true if has #!/', () => {
const result = hasInHash('#!/FooBar', isolateHash);
expect(result).toBe(true);
});
it('hasInHash should return false if does not have #!/', () => {
const result = hasInHash('#/FooBar', isolateHash);
expect(result).toBe(false);
});
it('hasInHash should return true if has #/', () => {
const result = hasInHash('#/FooBar', routeHash);
expect(result).toBe(true);
});
it('hasInHash should return false if does not have #/', () => {
const result = hasInHash('#!/FooBar', routeHash);
expect(result).toBe(false);
});
it('getHash should return FooBar', () => {
const result = getHash('#/FooBar', routeHash);
expect(result).toBe('FooBar');
});
it('getHash should return FooBar without params', () => {
const result = getHash('#/FooBar?id=Example/Perfect', routeHash);
expect(result).toBe('FooBar');
});
it('getHash should return decode value', () => {
const result = getHash('#!/Api%20%E7%BB%84%E4%BB%B6', isolateHash);
expect(result).toBe('Api 组件');
});
it('getHashAsArray should return array', () => {
const result = getHashAsArray('#!/FooBar/Component', isolateHash);
expect(result).toEqual(['FooBar', 'Component']);
});
it('getHashAsArray should return array with an encoded component name', () => {
const result = getHashAsArray('#/Documentation/Files/%40foo%2Fcomponents', routeHash);
expect(result).toEqual(['Documentation', 'Files', '@foo/components']);
});
it('getHashAsArray should return array without params', () => {
const result = getHashAsArray('#/FooBar/Component?id=Example/Perfect', routeHash);
expect(result).toEqual(['FooBar', 'Component']);
});
it('getParameterByName should return Example when has id param', () => {
const result = getParameterByName('#/FooBar/Component?id=Example', 'id');
expect(result).toBe('Example');
});
it('getParameterByName should return null when do not has params', () => {
const result = getParameterByName('#/FooBar/Component', 'id');
expect(result).toEqual(null);
});
it('getParameterByName should return null when do not has id params', () => {
const result = getParameterByName('#/FooBar/Component?foobar=3', 'id');
expect(result).toEqual(null);
});
});
================================================
FILE: src/client/utils/__tests__/processComponents.spec.ts
================================================
import deepfreeze from 'deepfreeze';
import processComponents from '../processComponents';
const options = { useRouterLinks: false };
describe('processComponents', () => {
it('should set components’ displayName to a name property', () => {
const components = deepfreeze([
{
props: {
displayName: 'Foo',
},
},
]);
const result = processComponents(components, options);
expect(result[0].name).toBe('Foo');
});
it('should calculate href', () => {
const components = deepfreeze([
{
slug: 'foo',
props: {
displayName: 'Foo',
},
},
]);
const result = processComponents(components, options);
expect(result[0].href).toBe('/#foo');
});
describe('should set visibleName property on the component', () => {
it('from an visibleName component prop if available', () => {
const components = deepfreeze([
{
props: {
displayName: 'Foo',
visibleName: 'Foo Bar',
},
},
]);
const result = processComponents(components, options);
expect(result[0].visibleName).toBe('Foo Bar');
});
it('from an displayName component prop if visibleName prop is not available', () => {
const components = deepfreeze([
{
props: {
displayName: 'Foo',
},
},
]);
const result = processComponents(components, options);
expect(result[0].visibleName).toBe('Foo');
});
});
it('should append @example doclet to all examples', () => {
const components = deepfreeze([
{
props: {
displayName: 'Foo',
examples: [1, 2] as any[],
example: [3, 4] as any[],
},
},
]);
const result = processComponents(components, options);
expect(result[0].props && result[0].props.examples).toEqual([1, 2, 3, 4]);
});
});
================================================
FILE: src/client/utils/__tests__/processSections.spec.ts
================================================
import deepfreeze from 'deepfreeze';
import processSections from '../processSections';
const sections = deepfreeze([
{
sections: [
{
name: 'Components',
slug: 'components',
components: [
{
slug: 'button',
props: {
displayName: 'Button',
},
},
],
},
],
},
]);
const options = { useRouterLinks: false, hashPath: [] };
describe('processSections', () => {
test('should recursively process all sections and components', () => {
const result = processSections(sections, options);
const sectionsExpected = result[0].sections || [];
const comp = sectionsExpected.length
? sectionsExpected[0].components && sectionsExpected[0].components[0]
: undefined;
expect(comp?.name).toBe('Button');
expect(comp?.href).toBe('/#button');
});
test('should set visibleName property on each section', () => {
const result = processSections(sections, options);
const sectionsExpected = result[0].sections || [];
expect(sectionsExpected[0].visibleName).toBe('Components');
});
test('should recursively process all nested sections when useRouterLinks is true has passed', () => {
const result = processSections([{ name: 'Component', sections: [{ name: 'Button' }] }], {
useRouterLinks: true,
});
expect(result?.[0].href).toBe('/#/Component');
expect(result?.[0].sections?.[0].href).toBe('/#/Component/Button');
});
});
================================================
FILE: src/client/utils/__tests__/renderStyleguide.spec.ts
================================================
import { render } from '@testing-library/react';
import renderStyleguide from '../renderStyleguide';
const dummyLocation = { hash: '', search: '', pathname: '' };
const styleguide = {
config: {
title: 'My Style Guide',
pagePerSection: false,
},
welcomeScreen: false,
patterns: ['components/**.js'],
sections: [
{
exampleMode: 'collapse',
usageMode: 'collapse',
slug: 'section',
components: [
{
slug: 'foo',
pathLine: 'components/foo.js',
filepath: 'components/foo.js',
props: {
displayName: 'Button',
description: 'Foo foo',
},
},
{
slug: 'bar',
pathLine: 'components/bar.js',
filepath: 'components/bar.js',
props: {
displayName: 'Image',
description: 'Bar bar',
},
},
],
},
],
} as any;
const codeRevision = 1;
const doc = {
title: 'test',
};
const history = {
replaceState: () => {},
};
test('should render the style guide', () => {
const { getByText } = render(
renderStyleguide(styleguide, codeRevision, dummyLocation, doc, history)
);
expect(getByText('components/foo.js')).toBeInTheDocument();
expect(getByText('components/bar.js')).toBeInTheDocument();
});
test('should change document title', () => {
renderStyleguide(styleguide, codeRevision, dummyLocation, doc, history);
expect(doc.title).toBe('My Style Guide');
});
test('should change document title in isolated mode', () => {
const location = { hash: '#!/Button', pathname: '', search: '' };
renderStyleguide(styleguide, codeRevision, location, doc, history);
expect(doc.title).toBe('Button — My Style Guide');
});
test('should remove #/ from the address bar', () => {
const location = { hash: '#/', pathname: '/pizza', search: '?foo=bar' };
const historyWithSpy = { replaceState: jest.fn() };
renderStyleguide(styleguide, codeRevision, location, doc, historyWithSpy);
expect(historyWithSpy.replaceState).toBeCalledWith('', 'My Style Guide', '/pizza?foo=bar');
});
================================================
FILE: src/client/utils/__tests__/splitExampleCode.spec.ts
================================================
import splitExampleCode from '../splitExampleCode';
describe('splitExampleCode', () => {
test('basic example', () => {
const result = splitExampleCode(`var a = 1;
React.createElement('i', null, a);`);
expect(result).toEqual({
head: 'var a = 1',
example: `var a = 1;
return (React.createElement('i', null, a));`,
});
});
test('JSX not only in the last expression', () => {
const result = splitExampleCode(`function Wrapper(ref) {
var children = ref.children;
return React.createElement('div', {id: 'foo'}, children);
}
;React.createElement(Wrapper, null,
React.createElement(Wrapper, null, React.createElement(Icon, {name: "plus"})),
React.createElement(Wrapper, null, React.createElement(Icon, {name: "clip"}))
)`);
expect(result).toEqual({
example: `function Wrapper(ref) {
var children = ref.children;
return React.createElement('div', {id: 'foo'}, children);
}
;
return (React.createElement(Wrapper, null,
React.createElement(Wrapper, null, React.createElement(Icon, {name: "plus"})),
React.createElement(Wrapper, null, React.createElement(Icon, {name: "clip"}))
));`,
head: `function Wrapper(ref) {
var children = ref.children;
return React.createElement('div', {id: 'foo'}, children);
}
`,
});
});
test('single expression', () => {
const result = splitExampleCode('pizza');
expect(result).toEqual({
head: '',
example: `;
return (pizza);`,
});
});
test('empty string', () => {
const result = splitExampleCode('');
expect(result).toEqual({
head: '',
example: '',
});
});
test('comment', () => {
const result = splitExampleCode('/* ~ */');
expect(result).toEqual({
head: '',
example: '/* ~ */',
});
});
test('error', () => {
const result = splitExampleCode('?');
expect(result).toEqual({
head: '',
example: '?',
});
});
});
================================================
FILE: src/client/utils/__tests__/transpileImports.spec.ts
================================================
import transpileImports from '../transpileImports';
describe('transpileImports', () => {
test('transpile default imports', () => {
const result = transpileImports(`import B from 'cat'`);
expect(result).toMatchInlineSnapshot(`
"const cat$0 = require('cat');
const B = cat$0.default || cat$0;"
`);
});
test('transpile named imports', () => {
const result = transpileImports(`import {B} from 'cat'`);
expect(result).toMatchInlineSnapshot(`
"const cat$0 = require('cat');
const B = cat$0.B;"
`);
});
test('transpile mixed imports', () => {
const result = transpileImports(`import A, {B} from 'cat'`);
expect(result).toMatchInlineSnapshot(`
"const cat$0 = require('cat');
const A = cat$0.default || cat$0;
const B = cat$0.B;"
`);
});
test('transpile multiple import statements', () => {
const result = transpileImports(`/**
* Some important comment
*/
import 'dog'
/* Less important comments */
import B from 'cat'
// Absolutely not important comment
import C from 'capybara'
import D from 'hamster' // One more comment
import E from 'snake'
`);
expect(result).toMatchInlineSnapshot(`
"/**
* Some important comment
*/
require('dog');
/* Less important comments */
const cat$0 = require('cat');
const B = cat$0.default || cat$0;
// Absolutely not important comment
const capybara$0 = require('capybara');
const C = capybara$0.default || capybara$0;
const hamster$0 = require('hamster');
const D = hamster$0.default || hamster$0; // One more comment
const snake$0 = require('snake');
const E = snake$0.default || snake$0;
"
`);
});
test('transpile multiline named imports without trailing comma', () => {
const result = transpileImports(`import {
B,
C
} from 'cat'
`);
expect(result).toMatchInlineSnapshot(`
"const cat$0 = require('cat');
const B = cat$0.B;
const C = cat$0.C;
"
`);
});
test('transpile multiline named imports with trailing comma', () => {
const result = transpileImports(`import {
B,
C,
} from 'cat'
`);
expect(result).toMatchInlineSnapshot(`
"const cat$0 = require('cat');
const B = cat$0.B;
const C = cat$0.C;
"
`);
});
describe.each([
['./cat/capybara/hamster', '__cat_capybara_hamster'],
['../cat/capybara/hamster', '___cat_capybara_hamster'],
['cat/capybara/hamster', 'cat_capybara_hamster'],
])('transpile default imports via relative path', (modulePath, transpiled) => {
test(`${modulePath}`, () => {
const result = transpileImports(`import B from '${modulePath}'`);
expect(result).toMatchInlineSnapshot(`
"const ${transpiled}$0 = require('${modulePath}');
const B = ${transpiled}$0.default || ${transpiled}$0;"
`);
});
});
test('return code if there are no imports', () => {
const code = ` `;
const result = transpileImports(code);
expect(result).toEqual(code);
});
test('return code if there is an import and a syntax error', () => {
const code = `import foo from 'foo';&`;
const result = transpileImports(code);
expect(result).toEqual(code);
});
});
================================================
FILE: src/client/utils/compileCode.ts
================================================
import { transform, TransformOptions } from 'buble';
import transpileImports from './transpileImports';
const compile = (code: string, config: TransformOptions): string => transform(code, config).code;
const startsWithJsx = (code: string): boolean => !!code.trim().match(/^);
const wrapCodeInFragment = (code: string): string => `${code} ;`;
/*
* 1. Wrap code in React Fragment if it starts with JSX element
* 2. Transform import statements into require() calls
* 3. Compile code using Buble
*/
export default function compileCode(
code: string,
compilerConfig: TransformOptions,
onError?: (err: Error) => void
): string {
try {
let compiledCode;
try {
compiledCode = compile(code, compilerConfig);
} catch (err) {
if (
err instanceof SyntaxError &&
err.message.startsWith('Adjacent JSX elements must be wrapped in an enclosing tag')
) {
const wrappedCode = startsWithJsx(code) ? wrapCodeInFragment(code) : code;
compiledCode = compile(wrappedCode, compilerConfig);
} else if (onError && err instanceof Error) {
onError(err);
}
}
return compiledCode ? transpileImports(compiledCode) : '';
} catch (err) {
if (onError && err instanceof Error) {
onError(err);
}
}
return '';
}
================================================
FILE: src/client/utils/filterComponentExamples.ts
================================================
import * as Rsg from '../../typings';
/**
* Return a copy of the given component with the examples array filtered
* to contain only the specified index:
* filterComponentExamples({ examples: [1,2,3], ...other }, 2) → { examples: [3], ...other }
*
* @param {object} component
* @param {number} index
* @returns {object}
*/
export default function filterComponentExamples(
component: Rsg.Component,
index: number
): Rsg.Component {
return {
...component,
props: {
...component.props,
examples:
component.props && component.props.examples ? [component.props.examples[index]] : [],
},
};
}
================================================
FILE: src/client/utils/filterComponentsByExactName.ts
================================================
import * as Rsg from '../../typings';
/**
* Filters list of components by component name.
*
* @param {Array} components
* @param {string} name
* @return {Array}
*/
export default function filterComponentsByExactName(
components: Rsg.Component[],
name: string
): Rsg.Component[] {
return components.filter(component => component.name === name);
}
================================================
FILE: src/client/utils/filterComponentsByName.ts
================================================
import getFilterRegExp from './getFilterRegExp';
import * as Rsg from '../../typings';
/**
* Fuzzy filters components list by component name.
*
* @param {array} components
* @param {string} query
* @return {array}
*/
export default function filterComponentsByName(
components: Rsg.Component[],
query: string
): Rsg.Component[] {
const regExp = getFilterRegExp(query);
return components.filter(({ name }) => regExp.test(name as string));
}
================================================
FILE: src/client/utils/filterComponentsInSectionsByExactName.ts
================================================
import filterComponentsByExactName from './filterComponentsByExactName';
import * as Rsg from '../../typings';
/**
* Recursively filters all components in all sections by component name.
*
* @param {object} sections
* @param {string} name
* @param {boolean} recursive
* @return {Array}
*/
export default function filterComponentsInSectionsByExactName(
sections: Rsg.Section[],
name: string,
recursive: boolean
): Rsg.Section[] {
const filteredSections: Rsg.Section[] = [];
sections.forEach(section => {
if (section.components) {
const filteredComponents = filterComponentsByExactName(section.components, name);
if (filteredComponents.length) {
filteredSections.push({
slug: section.slug,
exampleMode: section.exampleMode,
usageMode: section.usageMode,
components: filteredComponents,
});
}
}
if (section.sections && recursive) {
filteredSections.push(
...filterComponentsInSectionsByExactName(section.sections, name, recursive)
);
}
});
return filteredSections;
}
================================================
FILE: src/client/utils/filterSectionExamples.ts
================================================
import * as Rsg from '../../typings';
/**
* Return a copy of the given section with the examples array filtered
* to contain only the specified index
*
* @param {object} section
* @param {number} index
* @returns {object}
*/
export default function filterSectionExamples(section: Rsg.Section, index = -1): Rsg.Section {
const content = Array.isArray(section.content) ? [section.content[index]] : [];
return {
...section,
content,
};
}
================================================
FILE: src/client/utils/filterSectionsByName.ts
================================================
import getFilterRegExp from './getFilterRegExp';
import filterComponentsByName from './filterComponentsByName';
import * as Rsg from '../../typings';
/**
* Fuzzy filters sections by section or component name.
*
* @param {Array} sections
* @param {string} query
* @return {Array}
*/
export default function filterSectionsByName(
sections: Rsg.TOCItem[],
query: string
): Rsg.TOCItem[] {
const regExp = getFilterRegExp(query);
return sections
.map(section => {
return {
...section,
sections: section.sections ? filterSectionsByName(section.sections, query) : [],
components: section.components ? filterComponentsByName(section.components, query) : [],
};
})
.filter(
section =>
section.components.length > 0 ||
section.sections.length > 0 ||
regExp.test(section.name || '-')
);
}
================================================
FILE: src/client/utils/findSection.ts
================================================
import find from 'lodash/find';
import * as Rsg from '../../typings';
/**
* Recursively finds a section with a given name (exact match)
*
* @param {Array} sections
* @param {string} name
* @return {object}
*/
export default function findSection(
sections: Rsg.Section[],
name: string
): Rsg.Section | undefined {
// We're using Lodash because IE11 doesn't support Array.find.
const found = find(sections, { name });
if (found) {
return found;
}
for (let i = 0; i < sections.length; i++) {
const section = sections[i];
if (!section.sections || section.sections.length === 0) {
continue;
}
const foundInSubsection = findSection(section.sections, name);
if (foundInSubsection) {
return foundInSubsection;
}
}
return undefined;
}
================================================
FILE: src/client/utils/getAst.ts
================================================
import { Parser, Node, Options } from 'acorn';
export interface Program extends Node {
body: Node[];
}
export const ACORN_OPTIONS: Options = {
ecmaVersion: 2019,
sourceType: 'module',
};
/**
* Parse source code with Acorn and return AST, returns undefined in case of errors
*/
export default function getAst(code: string): Program | undefined {
try {
return (Parser.parse(code, {
...ACORN_OPTIONS,
// types of acorn are too simplistic and we have to use the body
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any) as Program;
} catch (err) {
return undefined;
}
}
================================================
FILE: src/client/utils/getComponent.ts
================================================
type Module = DefaultExport | { [name: string]: any };
interface DefaultExport {
default: any;
}
function isDefaultExport(module: Module): module is DefaultExport {
return !!module.default;
}
/**
* Given a component module and a name,
* return the appropriate export.
* See /docs/Components.md
*/
export default function getComponent(module: Module, name?: string): Module {
//
// If the module defines a default export, return that
// e.g.
//
// ```
// export default function Component() { ... }
// ```
//
if (isDefaultExport(module)) {
return module.default;
}
// If it is a CommonJS module which exports a function, return that
// e.g.
//
// ```
// function Component() { ... }
// module.exports = Component;
// ```
//
if (!module.__esModule && typeof module === 'function') {
return module;
}
// If the module exports just one named export, return that
// e.g.
//
// ```
// export function Component() { ... }
// ```
//
const namedExports = Object.keys(module);
if (namedExports.length === 1) {
return module[namedExports[0]];
}
// If the module exports a named export with the same name as the
// understood Component identifier, return that
// e.g.
//
// ```
// // /component.js
// export function someUtil() { ... }
// export Component() { ... } // if identifier is Component, return this named export
// ```
//
// Else return the module itself
//
return (name ? module[name] : undefined) || module;
}
================================================
FILE: src/client/utils/getFilterRegExp.ts
================================================
/**
* RegExp to fuzzy filter components list by component name.
*
* @param {string} query
* @return {RegExp}
*/
export default function getFilterRegExp(query: string): RegExp {
query = query
.replace(/[^a-z0-9]/gi, '')
.split('')
.join('.*');
return new RegExp(query, 'i');
}
================================================
FILE: src/client/utils/getInfoFromHash.ts
================================================
import { hasInHash, getHashAsArray } from './handleHash';
function hasDigitsOnly(item: string): boolean {
return item.match(/^\d+$/) !== null;
}
/**
* Returns an object containing component/section name and, optionally, an example index
* from hash part or page URL:
* #!/Button → { targetName: 'Button' }
* #!/Button/1 → { targetName: 'Button', targetIndex: 1 }
*
* @param {string} hash
* @returns {object}
*/
export default function getInfoFromHash(
hash: string
): {
isolate?: boolean;
hashArray?: string[];
targetName?: string;
targetIndex?: number;
} {
const shouldIsolate = hasInHash(hash, '#!/');
if (shouldIsolate || hasInHash(hash, '#/')) {
const hashArray = getHashAsArray(hash, shouldIsolate ? '#!/' : '#/');
const targetHash = hashArray[hashArray.length - 1];
return {
isolate: shouldIsolate,
hashArray: hashArray.filter(item => !hasDigitsOnly(item)),
targetName: hashArray[0],
targetIndex: hasDigitsOnly(targetHash) ? parseInt(targetHash, 10) : undefined,
};
}
return {};
}
================================================
FILE: src/client/utils/getPageTitle.ts
================================================
import get from 'lodash/get';
import { DisplayModes } from '../consts';
import * as Rsg from '../../typings';
/**
* Return page title:
* “Style Guide Title” for all components view;
* “Component Name — Style Guide Title” for isolated component or example view.
* “Section Name — Style Guide Title” for isolated section view.
*
* @param {object} sections
* @param {string} baseTitle
* @param {string} displayMode
* @return {string}
*/
export default function getPageTitle(
sections: Rsg.Section[],
baseTitle: string,
displayMode: string
): string {
if (displayMode === DisplayModes.notFound) {
return 'Page not found';
}
if (sections.length) {
if (
displayMode === DisplayModes.component ||
(displayMode === DisplayModes.example && sections[0].components)
) {
const name = get(sections[0], 'components.0.name', sections[0].name);
return `${name} — ${baseTitle}`;
} else if (displayMode === DisplayModes.section || displayMode === DisplayModes.example) {
return `${sections[0].name} — ${baseTitle}`;
}
}
return baseTitle;
}
================================================
FILE: src/client/utils/getRouteData.ts
================================================
import isFinite from 'lodash/isFinite';
import filterComponentExamples from './filterComponentExamples';
import filterComponentsInSectionsByExactName from './filterComponentsInSectionsByExactName';
import filterSectionExamples from './filterSectionExamples';
import findSection from './findSection';
import getInfoFromHash from './getInfoFromHash';
import { DisplayModes } from '../consts';
import * as Rsg from '../../typings';
/**
* Return sections / components / examples to show on a screen according to a current route.
*
* Default: show all sections and components.
* #!/Button: show only Button section or Button component
* #!/Button/1: show only the second example (index 1) of Button component
*
* @param {object} sections
* @param {string} hash
* @param {boolean} pagePerSection
* @returns {object}
*/
export default function getRouteData(
sections: Rsg.Section[],
hash: string,
pagePerSection = false
): { sections: Rsg.Section[]; displayMode: string } {
// Parse URL hash to check if the components list must be filtered
const infoFromHash = getInfoFromHash(hash);
// Name of the filtered component/section to show isolated (/#!/Button → Button)
let { targetName, hashArray } = infoFromHash;
const {
// Index of the fenced block example of the filtered component isolate (/#!/Button/1 → 1)
targetIndex,
isolate,
} = infoFromHash;
let displayMode = isolate ? DisplayModes.example : DisplayModes.all;
if (pagePerSection && !targetName && sections[0] && sections[0].name) {
// For default takes the first section when pagePerSection enabled
targetName = sections[0].name;
hashArray = [targetName];
}
if (targetName) {
let filteredSections: Rsg.Section[] = [];
if (pagePerSection && hashArray) {
// hashArray could be an array as ["Documentation", "Files", "Button"]
// each hashArray's element represent each section name with the same deep
// so it should be filter each section to trying to find each one of array on the same deep
hashArray.forEach((hashName, index) => {
// Filter the requested component if required but only on the first depth
// so in the next time of iteration, it will be trying to filter only on the second depth and so on
filteredSections = filterComponentsInSectionsByExactName(sections, hashName, !!isolate);
// If filteredSections exists, its because is an array of an component
// else it is an array of sections and depending his sectionDepth
// his children could be filtered or not
if (filteredSections.length) {
sections = filteredSections;
} else {
let section = findSection(sections, hashName);
if (section) {
// Only if hashName is the last of hashArray his children should be filtered
// because else there are possibilities to keep on filtering to try find the next section
const isLastHashName = !hashArray || !hashArray[index + 1];
// When sectionDepth is bigger than 0, their children should be filtered
const shouldFilterTheirChildren = (section.sectionDepth || 0) > 0 && isLastHashName;
if (shouldFilterTheirChildren) {
// Filter his sections and components
section = {
...section,
sections: [],
components: [],
};
}
sections = [section];
} else {
sections = [];
}
}
});
if (!sections.length) {
displayMode = DisplayModes.notFound;
}
// The targetName takes the last of hashArray
targetName = hashArray[hashArray.length - 1];
} else {
// Filter the requested component if required
filteredSections = filterComponentsInSectionsByExactName(sections, targetName, true);
if (filteredSections.length) {
sections = filteredSections;
displayMode = DisplayModes.component;
} else {
const section = findSection(sections, targetName);
sections = section ? [section] : [];
displayMode = DisplayModes.section;
}
}
// If a single component or section is filtered and a fenced block index is specified hide all other examples
if (isFinite(targetIndex)) {
if (filteredSections.length === 1) {
const filteredComponents = filteredSections[0].components;
sections = [
{
...filteredSections[0],
components:
filteredComponents && typeof targetIndex === 'number'
? [filterComponentExamples(filteredComponents[0], targetIndex)]
: [],
},
];
displayMode = DisplayModes.example;
} else if (sections.length === 1) {
sections = [filterSectionExamples(sections[0], targetIndex)];
displayMode = DisplayModes.example;
}
}
}
return { sections, displayMode };
}
================================================
FILE: src/client/utils/getUrl.ts
================================================
/* Returns the HashPath to be included in the isolated page view url */
function getCurrentHashPath(
stripFragment: RegExp,
stripTrailingSlash: RegExp,
currentHash: string
): string {
/*This pattern matches urls like http://hostname.com/#button etc.,
these urls are generated when we click on a component in the side nav-bar.
This will verify whether the first character after the '#' symbol is an alphanumeric char or "_".
this pattern used to validate the components names.*/
const hashUrlPattern = /^#[a-zA-Z0-9_]/; // Ex. matches "#button","#1button","#_button"
/* This pattern matches "#!/" string pattern in the 'currentHash' const
this url pattern is used to show isolated page view mode in this project. */
const isolatedPageViewUrlPattern = /^#!\//; // Ex. matches "#!/button"
if (hashUrlPattern.test(currentHash)) {
return '';
} else {
return currentHash && !isolatedPageViewUrlPattern.test(currentHash)
? currentHash.replace(stripFragment, '').replace(stripTrailingSlash, '') + '/'
: '';
}
}
/**
* Gets the URL fragment for an isolated or nochrome link.
*
* @param {string} $.currentHash The current hash fragment of the page
* @param {string} $.encodedName The URL encoded name of the component
* @return {string}
*/
function buildIsolatedOrNoChromeFragment({
currentHash,
encodedName,
}: {
currentHash: string;
encodedName: string;
}): string {
const stripFragment = /^#\/?/;
const stripTrailingSlash = /\/$/;
const currentHashPath = getCurrentHashPath(stripFragment, stripTrailingSlash, currentHash);
return `#!/${currentHashPath}${encodedName}`;
}
interface GetUrlOptions {
name: string;
slug: string;
/**
* Example index
*/
example: number;
anchor: boolean;
/**
* Isolated mode
*/
isolated: boolean;
/**
* No chrome? (Can be combined with anchor or isolated)
*/
nochrome: boolean;
/**
* Absolute URL? (Can be combined with other flags)
*/
absolute: boolean;
hashPath: string[] | false;
useSlugAsIdParam: boolean;
takeHash: boolean;
}
/**
* Get component / section URL.
*
* @param {GetUrlOptions} options
* @param location Location object (will use current page location by default)
* @return {string}
*/
export default function getUrl(
{
name,
slug,
example,
anchor,
isolated,
nochrome,
absolute,
hashPath,
useSlugAsIdParam,
takeHash,
}: Partial = {},
{
origin,
pathname,
hash = '',
}: {
origin: string;
pathname: string;
hash: string;
} = window.location
): string {
let url = pathname;
const currentHash = hash.indexOf('?') > -1 ? hash.substring(0, hash.indexOf('?')) : hash;
if (takeHash) {
url += currentHash;
}
if (nochrome) {
url += '?nochrome';
}
const encodedName = encodeURIComponent(name || '');
if (anchor) {
url += `#${slug}`;
} else if (isolated || nochrome) {
url += buildIsolatedOrNoChromeFragment({ currentHash, encodedName });
}
if (hashPath) {
let encodedHashPath = hashPath.map(encodeURIComponent);
if (!useSlugAsIdParam) {
encodedHashPath = [...encodedHashPath, encodedName];
}
url += `#/${encodedHashPath.join('/')}`;
}
if (useSlugAsIdParam) {
url += `?id=${slug}`;
}
if (example !== undefined) {
url += `/${example}`;
}
if (absolute) {
return origin + url;
}
return url;
}
================================================
FILE: src/client/utils/handleHash.ts
================================================
import escapeRegExp from 'lodash/escapeRegExp';
// We’re using this file to handle the hash to develop the routes, there are two types of hash '#/' and '#!/'
// However, it is a temporal solution because is necessary using a library third-party that it is his focus
// You can find more information here:
// https://github.com/styleguidist/react-styleguidist/pull/993
const defaultPrependHash = '#/';
const separator = '/';
const hashValRegexp = /(.*)\?/;
function trimHash(hash: string, prependHash?: string): string {
if (!hash) {
return '';
}
const regexp = new RegExp('^' + escapeRegExp(prependHash || defaultPrependHash), 'g');
return hash.replace(regexp, '');
}
const trimParams = (hash: string): string => {
const result = hashValRegexp.exec(hash);
return (result && result[1]) || hash;
};
/**
* If hash has a certain element
*
* @param {string} hash
* @param {string} search
* @return {boolean}
*/
export const hasInHash = (hash: string, search: string): boolean => {
return hash !== '' && hash.indexOf(search) > -1;
};
/**
* Get hash value without '#', prependHash and parameters
*
* @param {string} hash
* @param {string} prependHash
* @return {string}
*/
export const getHash = (hash: string, prependHash?: string) => {
return decodeURIComponent(trimParams(trimHash(hash, prependHash)));
};
/**
* Get hash value split into an Array.
*
* @param {string} hash
* @param {string} prependHash
* @return {Array.}
*/
export const getHashAsArray = (hash: string, prependHash?: string): string[] => {
return trimParams(trimHash(hash, prependHash))
.split(separator)
.map(decodeURIComponent);
};
/**
* Get a parameter by name in hash
*
* @param {string} hash
* @param {string} name
* @return {string}
*/
export const getParameterByName = (hash: string, name: string): string | null => {
name = name.replace(/[[\]]/g, '\\$&');
const regex = new RegExp('[?&]' + name + '(=([^]*)|&|#|$)');
const results = regex.exec(hash);
if (!results) {
return null;
}
if (!results[2]) {
return '';
}
return decodeURIComponent(results[2].replace(/\+/g, ' '));
};
================================================
FILE: src/client/utils/processComponents.ts
================================================
import * as Rsg from '../../typings';
import getUrl from './getUrl';
export interface HrefOptions {
hashPath?: string[];
useRouterLinks: boolean;
useHashId?: boolean;
}
/**
* Do things that are hard or impossible to do in a loader: we don’t have access to component name
* and props in the styleguide-loader because we’re using `require` to load the component module.
*
* @param {Array} components
* @return {Array}
*/
export default function processComponents(
components: Rsg.Component[],
{ useRouterLinks, useHashId, hashPath }: HrefOptions
): Rsg.Component[] {
return components.map(component => {
const newComponent: Rsg.Component = component.props
? {
...component,
// Add .name shortcuts for names instead of .props.displayName.
name: component.props.displayName,
visibleName: component.props.visibleName || component.props.displayName,
props: {
...component.props,
// Append @example doclet to all examples
examples: [...(component.props.examples || []), ...(component.props.example || [])],
},
href:
component.href ||
getUrl({
name: component.props.displayName,
slug: component.slug,
anchor: !useRouterLinks,
hashPath: useRouterLinks ? hashPath : false,
useSlugAsIdParam: useRouterLinks ? useHashId : false,
}),
}
: {};
return newComponent;
});
}
================================================
FILE: src/client/utils/processSections.ts
================================================
import * as Rsg from '../../typings';
import processComponents, { HrefOptions } from './processComponents';
import getUrl from './getUrl';
/**
* Recursively process each component in all sections.
*
* @param {Array} sections
* @return {Array}
*/
export default function processSections(
sections: Rsg.Section[],
{ useRouterLinks, useHashId = false, hashPath = [] }: HrefOptions
): Rsg.Section[] {
return sections.map((section) => {
const options = {
useRouterLinks: Boolean(useRouterLinks && section.name),
useHashId: section.sectionDepth === 0,
hashPath: [...hashPath, section.name ? section.name : '-'],
};
const href =
section.href ||
getUrl({
name: section.name,
slug: section.slug,
anchor: !useRouterLinks,
hashPath: useRouterLinks ? hashPath : false,
useSlugAsIdParam: useRouterLinks ? useHashId : false,
});
return {
...section,
// flag the section as an external link to avoid rendering it later
externalLink: !!section.href,
href,
visibleName: section.name,
components: processComponents(section.components || [], options),
sections: processSections(section.sections || [], options),
};
});
}
================================================
FILE: src/client/utils/renderStyleguide.tsx
================================================
import React from 'react';
import hashSum from 'hash-sum';
import slots from 'rsg-components/slots';
import StyleGuide from 'rsg-components/StyleGuide';
import getPageTitle from './getPageTitle';
import getRouteData from './getRouteData';
import processSections from './processSections';
import * as Rsg from '../../typings';
interface StyleguideObject {
sections: Rsg.Section[];
config: Rsg.ProcessedStyleguidistConfig;
patterns: string[];
welcomeScreen?: boolean;
}
/**
* @param {object} styleguide An object returned by styleguide-loader
* @param {number} codeRevision
* @param {Location} [loc]
* @param {Document} [doc]
* @param {History} [hist]
* @return {React.ReactElement}
*/
export default function renderStyleguide(
styleguide: StyleguideObject,
codeRevision: number,
loc: { hash: string; pathname: string; search: string } = window.location,
doc: { title: string } = document,
hist: { replaceState: (name: string, title: string, url: string) => void } = window.history
): React.ReactElement {
const allSections = processSections(styleguide.sections, {
useRouterLinks: styleguide.config.pagePerSection,
});
const { title, pagePerSection, theme, styles } = styleguide.config;
const { sections, displayMode } = getRouteData(allSections, loc.hash, pagePerSection);
// Update page title
doc.title = getPageTitle(sections, title, displayMode);
// If the current hash location was set to just `/` (e.g. when navigating back from isolated view to overview)
// replace the URL with one without hash, to present the user with a single address of the overview screen
if (loc.hash === '#/') {
const url = loc.pathname + loc.search;
hist.replaceState('', doc.title, url);
}
return (
);
}
================================================
FILE: src/client/utils/rewriteImports.ts
================================================
// Temporary copy to fix
// https://github.com/lukeed/rewrite-imports/issues/10
const UNNAMED = /import\s*['"]([^'"]+)['"];?/gi;
const NAMED = /import\s*(\*\s*as)?\s*(\w*?)\s*,?\s*(?:\{([\s\S]*?)\})?\s*from\s*['"]([^'"]+)['"];?/gi;
function alias(key: string): { key: string; name: string } {
key = key.trim();
const name = key.split(' as ');
if (name.length > 1) {
key = name.shift() as string;
}
return { key, name: name[0] };
}
let num: number;
function generate(keys: string[], dep: string, base: string, fn: string): string {
const tmp = dep.replace(/\W/g, '_') + '$' + num++; // uniqueness
const name = alias(tmp).name;
dep = `${fn}('${dep}')`;
let obj;
let out = `const ${name} = ${dep};`;
if (base) {
out += `\nconst ${base} = ${tmp}.default || ${tmp};`;
}
keys.forEach(key => {
obj = alias(key);
out += `\nconst ${obj.name} = ${tmp}.${obj.key};`;
});
return out;
}
export default function(str: string, fn = 'require'): string {
num = 0;
return str
.replace(NAMED, (_, asterisk, base, req: string | undefined, dep: string) =>
generate(req ? req.split(',').filter(d => d.trim()) : [], dep, base, fn)
)
.replace(UNNAMED, (_, dep) => `${fn}('${dep}');`);
}
================================================
FILE: src/client/utils/splitExampleCode.ts
================================================
import find from 'lodash/find';
import getAst from './getAst';
// Strip semicolon (;) at the end
const unsemicolon = (s: string): string => s.replace(/;\s*$/, '');
/**
* Take source code and returns:
* 1. Code before the last top-level expression.
* 2. Code with the last top-level expression wrapped in a return statement
* (kind of an implicit return).
*
* Example:
* var a = 1; React.createElement('i', null, a); // =>
* 1. var a = 1
* 2. var a = 1; return (React.createElement('i', null, a));
*/
export default function splitExampleCode(code: string): { head: string; example: string } {
const ast = getAst(code);
if (!ast) {
return { head: '', example: code };
}
const firstExpression = find(ast.body.reverse(), { type: 'ExpressionStatement' });
if (!firstExpression) {
return { head: '', example: code };
}
const { start, end } = firstExpression;
const head = unsemicolon(code.substring(0, start));
const firstExpressionCode = unsemicolon(code.substring(start, end));
const example = `${head};\nreturn (${firstExpressionCode});`;
return {
head,
example,
};
}
================================================
FILE: src/client/utils/transpileImports.ts
================================================
import { walk } from 'estree-walker';
import rewriteImports from './rewriteImports';
import getAst from './getAst';
const hasImports = (code: string): boolean => !!code.match(/import[\S\s]+?['"]([^'"]+)['"];?/m);
/**
* Replace ECMAScript imports with require() calls
*/
export default function transpileImports(code: string): string {
// Don't do anything when the code has nothing that looks like an import
if (!hasImports(code)) {
return code;
}
// Ignore errors, they should be caught by Buble
const ast = getAst(code);
if (!ast) {
return code;
}
let offset = 0;
// estree walkers type is incompatible with acorns output
// it is working here out of luck and typescript is demonstrating it
// we have to go through the any part to keep the nodes with their `node.start`
// and `node.stop`
// eslint-disable-next-line @typescript-eslint/no-explicit-any
walk(ast as any, {
// import foo from 'foo'
// import 'foo'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
enter: (node: any) => {
if (node.type === 'ImportDeclaration' && node.source) {
const start = node.start + offset;
const end = node.end + offset;
const statement = code.substring(start, end);
const transpiledStatement = rewriteImports(statement);
code = code.substring(0, start) + transpiledStatement + code.substring(end);
offset += transpiledStatement.length - statement.length;
}
},
});
return code;
}
================================================
FILE: src/loaders/__tests__/examples-loader.spec.ts
================================================
import examplesLoader from '../examples-loader';
/* eslint-disable no-new-func */
const query = {
file: '../foo.js',
displayName: 'FooComponent',
shouldShowDefaultExample: false,
};
const subComponentQuery = {
file: '../fooSub.js',
displayName: 'FooComponent.SubComponent',
shouldShowDefaultExample: false,
};
const getQueryOptions = (options = {}) => ({ ...query, ...options });
const getSubComponentQueryOptions = (options = {}) => ({ ...subComponentQuery, ...options });
it('should return valid, parsable JS', () => {
const exampleMarkdown = `
# header
text
\`\`\`
\`\`\`
`;
const result = examplesLoader.call(
{
getOptions: () => getQueryOptions(),
_styleguidist: {},
} as any,
exampleMarkdown
);
expect(result).toBeTruthy();
expect(() => new Function(result)).not.toThrowError(SyntaxError);
});
it('should replace all occurrences of __COMPONENT__ with provided query.displayName', () => {
const exampleMarkdown = `
<__COMPONENT__>
text
Name of component: __COMPONENT__
<__COMPONENT__ />
`;
const result = examplesLoader.call(
{
getOptions: () => getQueryOptions({ shouldShowDefaultExample: true }),
_styleguidist: {},
} as any,
exampleMarkdown
);
expect(result).not.toMatch(/__COMPONENT__/);
const componentHtml = result.match(/(.*?)<\/div>/);
expect(componentHtml && componentHtml[0]).toMatchInlineSnapshot(
`"
\\\\n\\\\t\\\\n\\\\t\\\\ttext \\\\n\\\\t\\\\tName of component: FooComponent \\\\n\\\\t \\\\n\\\\t \\\\n
"`
);
});
it('should pass updateExample function from config to chunkify', () => {
const exampleMarkdown = `
\`\`\`jsx static
Hello world!
\`\`\`
`;
const updateExample = jest.fn((props) => props);
examplesLoader.call(
{
getOptions: () => getQueryOptions(),
resourcePath: '/path/to/foo/examples/file',
_styleguidist: {
updateExample,
},
} as any,
exampleMarkdown
);
expect(updateExample).toBeCalledWith(
{
content: 'Hello world!',
settings: { static: true },
lang: 'jsx',
},
'/path/to/foo/examples/file'
);
});
it('should generate require map when require() is used', () => {
const exampleMarkdown = `
One:
const _ = require('lodash');
Two:
`;
const result = examplesLoader.call(
{
getOptions: () => getQueryOptions(),
_styleguidist: {},
} as any,
exampleMarkdown
);
expect(result).toBeTruthy();
expect(() => new Function(result)).not.toThrowError(SyntaxError);
expect(result).toMatch(`'lodash': require('lodash')`);
expect(result).toMatch(`'react': require('react')`);
});
it('should generate require map when import is used', () => {
const exampleMarkdown = `
One:
import _ from 'lodash';
`;
const result = examplesLoader.call(
{
getOptions: () => getQueryOptions(),
_styleguidist: {},
} as any,
exampleMarkdown
);
expect(result).toBeTruthy();
expect(() => new Function(result)).not.toThrowError(SyntaxError);
expect(result).toMatch(`'lodash': require('lodash')`);
expect(result).toMatch(`'react': require('react')`);
});
it('should work with multiple JSX element on the root level', () => {
const exampleMarkdown = `
`;
const result = examplesLoader.call(
{
getOptions: () => getQueryOptions(),
_styleguidist: {},
} as any,
exampleMarkdown
);
expect(result).toBeTruthy();
expect(() => new Function(result)).not.toThrowError(SyntaxError);
});
it('should prepend example code with React require()', () => {
const exampleMarkdown = ` `;
const result = examplesLoader.call(
{
getOptions: () => getQueryOptions(),
_styleguidist: {},
} as any,
exampleMarkdown
);
expect(result).toBeTruthy();
expect(() => new Function(result)).not.toThrowError(SyntaxError);
expect(result).toMatch(
`const React$0 = require('react');\\nconst React = React$0.default || (React$0['React'] || React$0);`
);
});
it('should prepend example code with component require()', () => {
const exampleMarkdown = ` `;
const result = examplesLoader.call(
{
getOptions: () => getQueryOptions(),
_styleguidist: {},
} as any,
exampleMarkdown
);
expect(result).toBeTruthy();
expect(() => new Function(result)).not.toThrowError(SyntaxError);
expect(result).toMatch(
`const FooComponent$0 = require('../foo.js');\\nconst FooComponent = FooComponent$0.default || (FooComponent$0['FooComponent'] || FooComponent$0);`
);
});
it('should prepend example code with root component require() for sub components', () => {
const exampleMarkdown = ` `;
const result = examplesLoader.call(
{
getOptions: () => getSubComponentQueryOptions(),
_styleguidist: {},
} as any,
exampleMarkdown
);
expect(result).toBeTruthy();
expect(() => new Function(result)).not.toThrowError(SyntaxError);
expect(result).toMatch(
`const FooComponentSubComponent$0 = require('../fooSub.js');\\nconst FooComponentSubComponent = FooComponentSubComponent$0.default || (FooComponentSubComponent$0['FooComponentSubComponent'] || FooComponentSubComponent$0);`
);
});
it('should allow explicit import of React and component module', () => {
const exampleMarkdown = `
import React from 'react';
import FooComponent from '../foo.js';
`;
const result = examplesLoader.call(
{
getOptions: () => getQueryOptions(),
_styleguidist: {},
} as any,
exampleMarkdown
);
expect(result).toBeTruthy();
expect(() => new Function(result)).not.toThrowError(SyntaxError);
expect(result).toMatch(
`const React$0 = require('react');\\nconst React = React$0.default || (React$0['React'] || React$0);`
);
expect(result).toMatch(
`const FooComponent$0 = require('../foo.js');\\nconst FooComponent = FooComponent$0.default || (FooComponent$0['FooComponent'] || FooComponent$0);`
);
});
it('should works for any Markdown file, without a current component', () => {
const exampleMarkdown = `
import React from 'react';
import FooComponent from '../foo.js';
`;
const result = examplesLoader.call(
{
getOptions: () => ({}),
_styleguidist: {},
} as any,
exampleMarkdown
);
expect(result).toBeTruthy();
expect(() => new Function(result)).not.toThrowError(SyntaxError);
expect(result).toMatch(
`const React$0 = require('react');\\nconst React = React$0.default || (React$0['React'] || React$0);`
);
expect(result).not.toMatch('undefined');
});
================================================
FILE: src/loaders/__tests__/props-loader.spec.ts
================================================
import vm from 'vm';
import { readFileSync } from 'fs';
import glogg from 'glogg';
import { PropDescriptor } from 'react-docgen';
import sortBy from 'lodash/sortBy';
import config from '../../scripts/schemas/config';
import propsLoader from '../props-loader';
const logger = glogg('rsg');
// eslint-disable-next-line @typescript-eslint/naming-convention
const _styleguidist = {
handlers: config.handlers.default,
getExampleFilename: config.getExampleFilename.default,
resolver: config.resolver.default,
};
it('should return valid, parsable JS', () => {
const file = './test/components/Button/Button.js';
const result = propsLoader.call(
{
request: file,
_styleguidist,
} as any,
readFileSync(file, 'utf8')
);
expect(result).toBeTruthy();
expect(() => new vm.Script(result)).not.toThrow();
});
it('should extract doclets', () => {
const file = './test/components/Placeholder/Placeholder.js';
const result = propsLoader.call(
{
request: file,
_styleguidist,
} as any,
readFileSync(file, 'utf8')
);
expect(result).toBeTruthy();
expect(() => new vm.Script(result)).not.toThrow();
expect(result.includes('makeABarrelRoll')).toBe(false);
expect(result).toMatch('getImageUrl');
expect(result).toMatch(/'see': '\{@link link\}'/);
expect(result).toMatch(/'link': 'link'/);
expect(result).toMatch(/require\('!!.*?\/loaders\/examples-loader\.js!\.\/examples.md'\)/);
});
describe('property sorting', () => {
it('should sort properties by default', () => {
const file = './test/components/Price/Price.js';
const result = propsLoader.call(
{
request: file,
_styleguidist,
} as any,
readFileSync(file, 'utf8')
);
expect(result).toBeTruthy();
expect(() => new vm.Script(result)).not.toThrow();
expect(result.includes('makeABarrelRoll')).toBe(false);
expect(result).toMatch(
/props[\s\S]*?name': 'symbol'[\s\S]*?name': 'value'[\s\S]*?name': 'emphasize'[\s\S]*?name': 'unit'/m
);
});
it('should be possible to disable sorting', () => {
const file = './test/components/Price/Price.js';
const result = propsLoader.call(
{
request: file,
_styleguidist: { ..._styleguidist, sortProps: (props: any) => props },
} as any,
readFileSync(file, 'utf8')
);
expect(result).toBeTruthy();
expect(() => new vm.Script(result)).not.toThrow();
expect(result.includes('makeABarrelRoll')).toBe(false);
expect(result).toMatch(
/props[\s\S]*?name': 'value'[\s\S]*?name': 'unit'[\s\S]*?name': 'emphasize'[\s\S]*?name': 'symbol'/m
);
});
it('should be possible to write custom sort function', () => {
const sortFn = (props: any) => {
const requiredProps = sortBy(
props.filter((prop: PropDescriptor) => prop.required),
'name'
).reverse();
const optionalProps = sortBy(
props.filter((prop: PropDescriptor) => !prop.required),
'name'
).reverse();
return optionalProps.concat(requiredProps);
};
const file = './test/components/Price/Price.js';
const result = propsLoader.call(
{
request: file,
_styleguidist: { ..._styleguidist, sortProps: sortFn },
} as any,
readFileSync(file, 'utf8')
);
expect(result).toBeTruthy();
expect(() => new vm.Script(result)).not.toThrow();
expect(result.includes('makeABarrelRoll')).toBe(false);
expect(result).toMatch(
/props[\s\S]*?name': 'unit'[\s\S]*?name': 'emphasize'[\s\S]*?name': 'value'[\s\S]*?name': 'symbol'/m
);
});
});
it('should work with JSDoc annnotated components', () => {
const file = './test/components/Annotation/Annotation.js';
const result = propsLoader.call(
{
request: file,
_styleguidist,
} as any,
readFileSync(file, 'utf8')
);
expect(result).toBeTruthy();
expect(() => new vm.Script(result)).not.toThrow();
// eslint-disable-next-line no-eval
expect(eval(result)).toEqual(
expect.objectContaining({
displayName: 'Annotation',
description: 'Styled-component test\n',
doclets: {
component: true,
},
})
);
});
it('should not render ignored props', () => {
const file = './test/components/Button/Button.js';
const result = propsLoader.call(
{
request: file,
_styleguidist,
} as any,
readFileSync(file, 'utf8')
);
expect(result).toBeTruthy();
expect(() => new vm.Script(result)).not.toThrow();
expect(result.includes('ignoredProp')).toBe(false);
});
it('should attach examples from Markdown file', () => {
const file = './test/components/Button/Button.js';
const result = propsLoader.call(
{
request: file,
_styleguidist,
} as any,
readFileSync(file, 'utf8')
);
expect(result).toBeTruthy();
expect(() => new vm.Script(result)).not.toThrow();
expect(result).toMatch(
/require\('!!.*?\/loaders\/examples-loader\.js\?displayName=Button&file=\.%2FButton\.js&shouldShowDefaultExample=false!test\/components\/Button\/Readme\.md'\)/
);
});
it('should warn if no componets are exported', () => {
const warn = jest.fn();
logger.once('warn', warn);
const file = __filename;
const result = propsLoader.call(
{
request: file,
_styleguidist,
} as any,
readFileSync(file, 'utf8')
);
expect(result).toBeTruthy();
expect(() => new vm.Script(result)).not.toThrow();
expect(warn).toBeCalledWith(expect.stringMatching('doesn’t export a component'));
});
it('should warn if a file cannot be parsed', () => {
const warn = jest.fn();
logger.once('warn', warn);
const file = './test/components/Button/Readme.md';
const result = propsLoader.call(
{
request: file,
_styleguidist,
} as any,
readFileSync(file, 'utf8')
);
expect(result).toBeTruthy();
expect(() => new vm.Script(result)).not.toThrow();
expect(warn).toBeCalledWith(expect.stringMatching('Cannot parse'));
});
it('should add context dependencies to webpack from contextDependencies config option', () => {
const contextDependencies = ['foo', 'bar'];
const addContextDependency = jest.fn();
const file = './test/components/Button/Button.js';
const result = propsLoader.call(
{
request: file,
_styleguidist: { ..._styleguidist, contextDependencies },
addContextDependency,
} as any,
readFileSync(file, 'utf8')
);
expect(() => new vm.Script(result)).not.toThrow();
expect(addContextDependency).toHaveBeenCalledTimes(2);
expect(addContextDependency).toBeCalledWith(contextDependencies[0]);
expect(addContextDependency).toBeCalledWith(contextDependencies[1]);
});
it('should update the returned props object after enhancing from the updateDocs config option', () => {
const updateDocs = jest.fn();
const file = './test/components/Button/Button.js';
const result = propsLoader.call(
{
request: file,
_styleguidist: { ..._styleguidist, updateDocs },
} as any,
readFileSync(file, 'utf8')
);
expect(() => new vm.Script(result)).not.toThrow();
expect(updateDocs).toHaveBeenCalledWith(
expect.objectContaining({ displayName: 'Button' }),
'./test/components/Button/Button.js'
);
});
================================================
FILE: src/loaders/__tests__/styleguide-loader.spec.ts
================================================
import vm from 'vm';
import path from 'path';
import * as styleguideLoader from '../styleguide-loader';
import getConfig from '../../scripts/config';
/* eslint-disable quotes */
const file = path.resolve(__dirname, '../../../test/components/Button/Button.js');
const configDir = path.resolve(__dirname, '../../../test');
it('should return valid, parsable JS', () => {
const result = styleguideLoader.pitch.call({
request: file,
_styleguidist: {
sections: [{ components: 'components/**/*.js' }],
configDir,
getExampleFilename: () => 'Readme.md',
getComponentPathLine: (filepath: string) => filepath,
},
addContextDependency: () => {},
} as any);
expect(result).toBeTruthy();
expect(() => new vm.Script(result)).not.toThrow();
});
it('should return correct component paths: default glob pattern', () => {
const result = styleguideLoader.pitch.call({
request: file,
_styleguidist: {
...getConfig(path.resolve(__dirname, '../../../test/apps/defaults/styleguide.config.js')),
},
addContextDependency: () => {},
} as any);
expect(result).toBeTruthy();
expect(() => new vm.Script(result)).not.toThrow();
expect(result).toMatch(`'filepath': 'src/components/Button.js'`);
expect(result).toMatch(`'filepath': 'src/components/Placeholder.js'`);
});
it('should return correct component paths: glob', () => {
const result = styleguideLoader.pitch.call({
request: file,
_styleguidist: {
sections: [{ components: 'components/**/*.js' }],
configDir,
getExampleFilename: () => 'Readme.md',
getComponentPathLine: (filepath: string) => filepath,
},
addContextDependency: () => {},
} as any);
expect(result).toBeTruthy();
expect(() => new vm.Script(result)).not.toThrow();
expect(result).toMatch(`'filepath': 'components/Button/Button.js'`);
expect(result).toMatch(`'filepath': 'components/Placeholder/Placeholder.js'`);
expect(result).toMatch(`'filepath': 'components/RandomButton/RandomButton.js'`);
});
it('should return correct component paths: function returning absolute paths', () => {
const result = styleguideLoader.pitch.call({
request: file,
_styleguidist: {
sections: [
{
components: () => [
`${configDir}/components/Button/Button.js`,
`${configDir}/components/Placeholder/Placeholder.js`,
],
},
],
configDir,
getExampleFilename: () => 'Readme.md',
getComponentPathLine: (filepath: string) => filepath,
},
addContextDependency: () => {},
} as any);
expect(result).toBeTruthy();
expect(() => new vm.Script(result)).not.toThrow();
expect(result).toMatch(`'filepath': 'components/Button/Button.js'`);
expect(result).toMatch(`'filepath': 'components/Placeholder/Placeholder.js'`);
expect(result).not.toMatch(`'filepath': 'components/RandomButton/RandomButton.js'`);
});
it('should return correct component paths: function returning relative paths', () => {
const result = styleguideLoader.pitch.call({
request: file,
_styleguidist: {
sections: [
{
components: () => [
'components/Button/Button.js',
'components/Placeholder/Placeholder.js',
],
},
],
configDir,
getExampleFilename: () => 'Readme.md',
getComponentPathLine: (filepath: string) => filepath,
},
addContextDependency: () => {},
} as any);
expect(result).toBeTruthy();
expect(() => new vm.Script(result)).not.toThrow();
expect(result).toMatch(`'filepath': 'components/Button/Button.js'`);
expect(result).toMatch(`'filepath': 'components/Placeholder/Placeholder.js'`);
expect(result).not.toMatch(`'filepath': 'components/RandomButton/RandomButton.js'`);
});
it('should return correct component paths: array of of relative paths', () => {
const result = styleguideLoader.pitch.call({
request: file,
_styleguidist: {
sections: [
{
components: ['components/Button/Button.js', 'components/Placeholder/Placeholder.js'],
},
],
configDir,
getExampleFilename: () => 'Readme.md',
getComponentPathLine: (filepath: string) => filepath,
},
addContextDependency: () => {},
} as any);
expect(result).toBeTruthy();
expect(() => new vm.Script(result)).not.toThrow();
expect(result).toMatch(`'filepath': 'components/Button/Button.js'`);
expect(result).toMatch(`'filepath': 'components/Placeholder/Placeholder.js'`);
});
it('should filter out components without examples if skipComponentsWithoutExample=true', () => {
const result = styleguideLoader.pitch.call({
request: file,
_styleguidist: {
sections: [
{
components: () => [
'components/Button/Button.js',
'components/RandomButton/RandomButton.js',
],
},
],
configDir,
skipComponentsWithoutExample: true,
getExampleFilename: (componentPath: string) =>
path.join(path.dirname(componentPath), 'Readme.md'),
getComponentPathLine: (filepath: string) => filepath,
},
addContextDependency: () => {},
} as any);
expect(result).toBeTruthy();
expect(() => new vm.Script(result)).not.toThrow();
expect(result).toMatch(`'filepath': 'components/Button/Button.js'`);
expect(result).not.toMatch(`'filepath': 'components/RandomButton/RandomButton.js'`);
});
it('should add context dependencies to webpack from contextDependencies config option', () => {
const contextDependencies = ['foo', 'bar'];
const addContextDependency = jest.fn();
styleguideLoader.pitch.call({
request: file,
_styleguidist: {
sections: [{ components: 'components/**/*.js' }],
configDir,
getExampleFilename: () => 'Readme.md',
getComponentPathLine: (filepath: string) => filepath,
contextDependencies,
},
addContextDependency,
} as any);
expect(addContextDependency).toHaveBeenCalledTimes(2);
expect(addContextDependency).toBeCalledWith(contextDependencies[0]);
expect(addContextDependency).toBeCalledWith(contextDependencies[1]);
});
it('should add common parent folder of all components to context dependencies', () => {
const addContextDependency = jest.fn();
styleguideLoader.pitch.call({
request: file,
_styleguidist: {
sections: [{ components: 'components/**/*.js' }],
configDir,
getExampleFilename: () => 'Readme.md',
getComponentPathLine: (filepath: string) => filepath,
},
addContextDependency,
} as any);
expect(addContextDependency).toHaveBeenCalledTimes(1);
expect(addContextDependency).toBeCalledWith(expect.stringMatching(/test[\\/]components[\\//]$/));
});
it('should convert styles and themes as string into requireIt objects', () => {
const result = styleguideLoader.pitch.call({
request: file,
_styleguidist: {
sections: [],
styles: 'path/to/styles',
theme: 'path/to/theme',
},
addDependency: jest.fn(),
} as any);
expect(result).toMatch(/require\('path\/to\/styles'\)/);
expect(result).toMatch(/require\('path\/to\/theme'\)/);
});
it('should flag both styles and theme as dependencies', () => {
const addDependency = jest.fn();
styleguideLoader.pitch.call({
request: file,
_styleguidist: {
sections: [],
styles: 'path/to/styles',
theme: 'path/to/theme',
},
addDependency,
} as any);
expect(addDependency).toHaveBeenCalledWith('path/to/styles');
expect(addDependency).toHaveBeenCalledWith('path/to/theme');
});
it('should transform styles into ES module compatible imports', () => {
const result = styleguideLoader.pitch.call({
request: file,
_styleguidist: {
sections: [],
styles: 'path/to/styles',
},
addDependency: jest.fn(),
} as any);
expect(result).toMatchInlineSnapshot(`
"const __rsgStyles$0 = require('path/to/styles');
const __rsgStyles = __rsgStyles$0.default || (__rsgStyles$0['__rsgStyles'] || __rsgStyles$0);
if (module.hot) {
module.hot.accept([])
}
module.exports = {
'config': { 'styles': __rsgStyles },
'welcomeScreen': true,
'patterns': [],
'sections': []
}
"
`);
});
================================================
FILE: src/loaders/examples-loader.ts
================================================
import path from 'path';
import filter from 'lodash/filter';
import map from 'lodash/map';
import values from 'lodash/values';
import flatten from 'lodash/flatten';
import { generate } from 'escodegen';
import toAst from 'to-ast';
import { builders as b } from 'ast-types';
import chunkify from './utils/chunkify';
import expandDefaultComponent from './utils/expandDefaultComponent';
import getImports from './utils/getImports';
import requireIt from './utils/requireIt';
import resolveESModule from './utils/resolveESModule';
import * as Rsg from '../typings';
const absolutize = (filepath: string) => path.resolve(__dirname, filepath);
const REQUIRE_IN_RUNTIME_PATH = absolutize('utils/client/requireInRuntime');
const EVAL_IN_CONTEXT_PATH = absolutize('utils/client/evalInContext');
export default function examplesLoader(this: Rsg.StyleguidistLoaderContext, source: string) {
const config = this._styleguidist;
const { file, displayName, shouldShowDefaultExample, customLangs } = this.getOptions();
// Replace placeholders (__COMPONENT__) with the passed-in component name
if (shouldShowDefaultExample) {
source = expandDefaultComponent(source, displayName);
}
const updateExample = config.updateExample
? (props: Omit) => config.updateExample(props, this.resourcePath)
: undefined;
// Load examples
const examples = chunkify(source, updateExample, customLangs);
// Find all import statements and require() calls in examples to make them
// available in webpack context at runtime.
// Note that we can't just use require() directly at runtime,
// because webpack changes its name to something like __webpack__require__().
const allCodeExamples = filter(examples, { type: 'code' });
const requiresFromExamples = allCodeExamples.reduce((requires: string[], example) => {
return requires.concat(getImports(example.content));
}, []);
// Auto imported modules.
// We don't need to do anything here to support explicit imports: they will
// work because both imports (generated below and by rewrite-imports) will
// be eventually transpiled to `var x = require('x')`, so we'll just have two
// of them in the same scope, which is fine in non-strict mode
const fullContext = {
// Modules, provied by the user
...config.context,
// Append React, because it’s required for JSX
React: 'react',
// Append the current component module to make it accessible in examples
// without an explicit import
// TODO: Do not leak absolute path
...(displayName ? { [displayName]: file } : {}),
};
// All required or imported modules, either explicitly in examples code
// or implicitly (React, current component and context config option)
const allModules = [...requiresFromExamples, ...values(fullContext)];
// “Prerequire” modules required in Markdown examples and context so they
// end up in a bundle and be available at runtime
const allModulesCode = allModules.reduce(
(requires: Record, requireRequest) => {
requires[requireRequest] = requireIt(requireRequest);
return requires;
},
{}
);
// Require context modules so they are available in an example
const requireContextCode = b.program(flatten(map(fullContext, resolveESModule)));
// Stringify examples object except the evalInContext function
const examplesWithEval: (Rsg.RuntimeCodeExample | Rsg.MarkdownExample)[] = examples.map(
(example) => {
if (example.type === 'code') {
return { ...example, evalInContext: { toAST: () => b.identifier('evalInContext') } as any };
} else {
return example;
}
}
);
return `
if (module.hot) {
module.hot.accept([])
}
var requireMap = ${generate(toAst(allModulesCode))};
var requireInRuntimeBase = require(${JSON.stringify(REQUIRE_IN_RUNTIME_PATH)}).default;
var requireInRuntime = requireInRuntimeBase.bind(null, requireMap);
var evalInContextBase = require(${JSON.stringify(absolutize(EVAL_IN_CONTEXT_PATH))}).default;
var evalInContext = evalInContextBase.bind(null, ${JSON.stringify(
generate(requireContextCode)
)}, requireInRuntime);
module.exports = ${generate(toAst(examplesWithEval))}
`;
}
================================================
FILE: src/loaders/props-loader.ts
================================================
import path from 'path';
import isArray from 'lodash/isArray';
import { Handler, parse, DocumentationObject, PropDescriptor } from 'react-docgen';
import { ASTNode } from 'ast-types';
import { NodePath } from 'ast-types/lib/node-path';
import { generate } from 'escodegen';
import toAst from 'to-ast';
import createLogger from 'glogg';
import getExamples from './utils/getExamples';
import getProps from './utils/getProps';
import defaultSortProps from './utils/sortProps';
import * as consts from '../scripts/consts';
import * as Rsg from '../typings';
const logger = createLogger('rsg');
const ERROR_MISSING_DEFINITION = 'No suitable component definition found.';
export default function (this: Rsg.StyleguidistLoaderContext, source: string) {
const file: string = this.request.split('!').pop() || '';
const config = this._styleguidist;
// Setup Webpack context dependencies to enable hot reload when adding new files or updating any of component dependencies
if (config.contextDependencies) {
config.contextDependencies.forEach((dir) => this.addContextDependency(dir));
}
const defaultParser = (
filePath: string,
code: string,
resolver: (
ast: ASTNode,
parser: { parse: (input: string) => ASTNode }
) => NodePath | NodePath[],
handlers: Handler[]
) => parse(code, resolver, handlers, { filename: filePath });
const propsParser = config.propsParser || defaultParser;
let docs: DocumentationObject = {};
try {
docs = propsParser(file, source, config.resolver, config.handlers(file));
// Support only one component
if (isArray(docs)) {
if (docs.length === 0) {
throw new Error(ERROR_MISSING_DEFINITION);
}
docs = docs[0];
}
} catch (err) {
if (err instanceof Error) {
const errorMessage = err.toString();
const componentPath = path.relative(process.cwd(), file);
const message =
errorMessage === `Error: ${ERROR_MISSING_DEFINITION}`
? `${componentPath} matches a pattern defined in “components” or “sections” options in your ` +
'style guide config but doesn’t export a component.\n\n' +
'It usually happens when using third-party libraries, see possible solutions here:\n' +
`${consts.DOCS_THIRDPARTIES}`
: `Cannot parse ${componentPath}: ${err}\n\n` +
'It usually means that react-docgen does not understand your source code, try to file an issue here:\n' +
'https://github.com/reactjs/react-docgen/issues';
logger.warn(message);
}
}
const tempDocs = getProps(docs, file);
let finalDocs: Rsg.PropsObject = { ...tempDocs, props: [] };
const componentProps = tempDocs.props;
if (componentProps) {
// Transform the properties to an array. This will allow sorting
// TODO: Extract to a module
const propsAsArray = Object.keys(componentProps).reduce((acc: PropDescriptor[], name) => {
componentProps[name].name = name;
acc.push(componentProps[name]);
return acc;
}, []);
const sortProps = config.sortProps || defaultSortProps;
finalDocs.props = sortProps(propsAsArray);
}
// Examples from Markdown file
const examplesFile = config.getExampleFilename(file);
finalDocs.examples = getExamples(
file,
finalDocs.displayName,
examplesFile,
config.defaultExample
);
if (config.updateDocs) {
finalDocs = config.updateDocs(finalDocs, file);
}
return `
if (module.hot) {
module.hot.accept([])
}
module.exports = ${generate(toAst(finalDocs))}
`;
}
================================================
FILE: src/loaders/styleguide-loader.ts
================================================
import pick from 'lodash/pick';
import flatten from 'lodash/flatten';
import { namedTypes as t, builders as b } from 'ast-types';
import commonDir from 'common-dir';
import { generate } from 'escodegen';
import toAst from 'to-ast';
import createLogger from 'glogg';
import * as fileExistsCaseInsensitive from '../scripts/utils/findFileCaseInsensitive';
import getAllContentPages from './utils/getAllContentPages';
import getComponentFilesFromSections from './utils/getComponentFilesFromSections';
import getComponentPatternsFromSections from './utils/getComponentPatternsFromSections';
import getSections from './utils/getSections';
import filterComponentsWithExample from './utils/filterComponentsWithExample';
import slugger from './utils/slugger';
import resolveESModule from './utils/resolveESModule';
import * as Rsg from '../typings';
const logger = createLogger('rsg');
// Config options that should be passed to the client
const CLIENT_CONFIG_OPTIONS = [
'compilerConfig',
'tocMode',
'mountPointId',
'pagePerSection',
'previewDelay',
'ribbon',
'showSidebar',
'styles',
'theme',
'title',
'version',
];
const STYLE_VARIABLE_NAME = '__rsgStyles';
const THEME_VARIABLE_NAME = '__rsgTheme';
export default function() {}
export function pitch(this: Rsg.StyleguidistLoaderContext) {
// Clear cache so it would detect new or renamed files
fileExistsCaseInsensitive.clearCache();
// Reset slugger for each code reload to be deterministic
slugger.reset();
const config = this._styleguidist;
let sections = getSections(config.sections, config);
if (config.skipComponentsWithoutExample) {
sections = filterComponentsWithExample(sections);
}
const allComponentFiles = getComponentFilesFromSections(
config.sections,
config.configDir,
config.ignore
);
const allContentPages = getAllContentPages(sections);
// Nothing to show in the style guide
const welcomeScreen = allContentPages.length === 0 && allComponentFiles.length === 0;
const patterns = welcomeScreen ? getComponentPatternsFromSections(config.sections) : undefined;
logger.debug('Loading components:\n' + allComponentFiles.join('\n'));
// Setup Webpack context dependencies to enable hot reload when adding new files
if (config.contextDependencies) {
config.contextDependencies.forEach((dir: string) => this.addContextDependency(dir));
} else if (allComponentFiles.length > 0) {
// Use common parent directory of all components as a context
this.addContextDependency(commonDir(allComponentFiles));
}
const configClone = { ...config };
const styleContext: t.VariableDeclaration[][] = [];
/**
* Transforms a string variable member of config
* it transforms this code
* ```
* {
* param: 'test/path'
* }
* ```
* into this code
* ```
* {
* param: require('test/path')
* }
* ```
*
* because we have to account for ES module exports,
* we add an extra step and transform it into aa statement
* that can import es5 `module.exports` and ES modules `export default`
*
* so the code will ultimtely look like this
*
* ```
* // es5 - es modules compatibility code
* var obj$0 = require('test/path')
* var obj = obj$0.default || obj$0
*
* {
* param: obj
* }
* ```
*
* @param memberName the name of the member of the object ("param" in the examples)
* @param varName the name of the variable to use ("obj" in the last example)
*/
const setVariableValueToObjectInFile = (
memberName: keyof Rsg.ProcessedStyleguidistCSSConfig,
varName: string
) => {
const configMember = config[memberName];
if (typeof configMember === 'string') {
// first attach the file as a dependency
this.addDependency(configMember);
// then create a variable to contain the value of the theme/style
styleContext.push(resolveESModule(configMember, varName));
// Finally assign the calculted value to the member of the clone
// NOTE: if we are mutating the config object without cloning it,
// it changes the value for all hmr iteration
// until the process is stopped.
const variableAst = {};
// Then override the `toAST()` function, because we know
// what the output of it should be, an identifier
Object.defineProperty(variableAst, 'toAST', {
enumerable: false,
value(): t.ASTNode {
return b.identifier(varName);
},
});
configClone[memberName] = variableAst;
}
};
setVariableValueToObjectInFile('styles', STYLE_VARIABLE_NAME);
setVariableValueToObjectInFile('theme', THEME_VARIABLE_NAME);
const styleguide = {
config: pick(configClone, CLIENT_CONFIG_OPTIONS),
welcomeScreen,
patterns,
sections,
};
return `${generate(b.program(flatten(styleContext)))}
if (module.hot) {
module.hot.accept([])
}
module.exports = ${generate(toAst(styleguide))}
`;
}
================================================
FILE: src/loaders/utils/__tests__/.eslintrc
================================================
{
"extends": "tamia/typescript"
}
================================================
FILE: src/loaders/utils/__tests__/__snapshots__/chunkify.spec.ts.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should even parse examples with custom extensions 1`] = `
Array [
Object {
"content": "Custom extensions",
"type": "markdown",
},
Object {
"content": "Example in vue ",
"settings": Object {},
"type": "code",
},
]
`;
exports[`should parse examples settings correctly 1`] = `
Array [
Object {
"content": "Pass props to CodeRenderer",
"type": "markdown",
},
Object {
"content": "Hello Markdown! ",
"settings": Object {
"showcode": true,
},
"type": "code",
},
Object {
"content": "Example in frame and Without editor ",
"settings": Object {
"frame": Object {
"width": "400px",
},
},
"type": "code",
},
Object {
"content": "Pass props to PreviewRenderer",
"type": "markdown",
},
Object {
"content": "Hello Markdown! ",
"settings": Object {
"noeditor": true,
},
"type": "code",
},
Object {
"content": "\`\`\`jsx
< h2> This is Highlighted! </ h2>
\`\`\`",
"type": "markdown",
},
]
`;
exports[`should parse undefined custom extensions without throwing 1`] = `
Array [
Object {
"content": "Undefined extensions (default)",
"type": "markdown",
},
Object {
"content": "Example in jsx with undefined extensions ",
"settings": Object {},
"type": "code",
},
Object {
"content": "\`\`\`pizza
Example in pizza with undefined extensions (test double)
\`\`\`",
"type": "markdown",
},
]
`;
exports[`should separate Markdown and component examples 1`] = `
Array [
Object {
"content": "# Header
Text with *some* **formatting** and a [link](/foo).
And some HTML.

This code example should be rendered as a playground:",
"type": "markdown",
},
Object {
"content": "Hello Markdown! ",
"settings": Object {},
"type": "code",
},
Object {
"content": "Text with some \`code\` (playground too).",
"type": "markdown",
},
Object {
"content": "Hello Markdown! ",
"settings": Object {},
"type": "code",
},
Object {
"content": "And some language and modifier (playground again):",
"type": "markdown",
},
Object {
"content": "Hello Markdown! ",
"settings": Object {
"noeditor": true,
},
"type": "code",
},
Object {
"content": "This should be just highlighted:
\`\`\`jsx
< h4> Hello Markdown! </ h4>
\`\`\`
This should be highlighted too:
\`\`\`html
< h5> Hello Markdown!</ h5>
\`\`\`",
"type": "markdown",
},
]
`;
================================================
FILE: src/loaders/utils/__tests__/__snapshots__/filterComponentsWithExample.spec.ts.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should skip components without example file 1`] = `
Array [
Object {
"components": Array [],
"content": "Readme.md",
"name": "Readme",
"sections": Array [],
},
Object {
"components": Array [
Object {
"filepath": "components/Button/Button.js",
"hasExamples": "require()",
},
],
"name": "Components",
"sections": Array [],
},
Object {
"components": Array [],
"name": "Nesting",
"sections": Array [
Object {
"components": Array [
Object {
"filepath": "components/Modal/Modal.js",
"hasExamples": "require()",
},
],
"name": "Nested",
"sections": Array [],
},
],
},
]
`;
================================================
FILE: src/loaders/utils/__tests__/__snapshots__/getAst.spec.ts.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`getAst accept Acorn plugins 1`] = `
Node {
"body": Array [
Node {
"end": 5,
"expression": Node {
"children": Array [],
"closingElement": null,
"end": 5,
"openingElement": Node {
"attributes": Array [],
"end": 5,
"name": Node {
"end": 2,
"name": "X",
"start": 1,
"type": "JSXIdentifier",
},
"selfClosing": true,
"start": 0,
"type": "JSXOpeningElement",
},
"start": 0,
"type": "JSXElement",
},
"start": 0,
"type": "ExpressionStatement",
},
],
"end": 5,
"sourceType": "module",
"start": 0,
"type": "Program",
}
`;
exports[`getAst return AST 1`] = `
Node {
"body": Array [
Node {
"end": 2,
"expression": Node {
"end": 2,
"raw": "42",
"start": 0,
"type": "Literal",
"value": 42,
},
"start": 0,
"type": "ExpressionStatement",
},
],
"end": 2,
"sourceType": "module",
"start": 0,
"type": "Program",
}
`;
================================================
FILE: src/loaders/utils/__tests__/__snapshots__/getComponents.spec.ts.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`getComponents() should return an object for components 1`] = `
Array [
Object {
"filepath": "../../Foo.js",
"hasExamples": false,
"metadata": Object {},
"module": Object {
"require": "Foo.js",
},
"pathLine": "../../Foo.js",
"props": Object {
"require": "!!~/src/loaders/props-loader.js!Foo.js",
},
"slug": "foo",
},
Object {
"filepath": "../../Bar.js",
"hasExamples": false,
"metadata": Object {},
"module": Object {
"require": "Bar.js",
},
"pathLine": "../../Bar.js",
"props": Object {
"require": "!!~/src/loaders/props-loader.js!Bar.js",
},
"slug": "bar",
},
]
`;
================================================
FILE: src/loaders/utils/__tests__/__snapshots__/getProps.spec.ts.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should accept @return as a synonym of @returns 1`] = `
Array [
Object {
"docblock": "
Baz method with foo param
@public
@return {string} test
",
"returns": Object {
"description": "test",
"title": "return",
"type": Object {
"name": "string",
"type": "NameExpression",
},
},
"tags": Object {
"public": Array [
Object {
"description": null,
"title": "public",
"type": null,
},
],
},
},
]
`;
exports[`should get method info from docblock and merge it 1`] = `
Array [
Object {
"docblock": "
Baz method with foo param
@public
@returns {string} test
",
"returns": Object {
"description": "test",
"title": "returns",
"type": Object {
"name": "string",
"type": "NameExpression",
},
},
"tags": Object {
"public": Array [
Object {
"description": null,
"title": "public",
"type": null,
},
],
},
},
]
`;
exports[`should get method params info from docblock and merge it with passed method info 1`] = `
Array [
Object {
"docblock": "
Foo method with baz param
@public
@param {string} [baz=bar]
@arg {string} foo param described with @arg tag
@argument {string} test param described with @argument tag
@returns {string} test
",
"params": Array [
Object {
"default": "bar",
"description": null,
"name": "baz",
"title": "param",
"type": Object {
"expression": Object {
"name": "string",
"type": "NameExpression",
},
"type": "OptionalType",
},
},
Object {
"description": "param described with @arg tag",
"name": "foo",
"title": "arg",
"type": Object {
"name": "string",
"type": "NameExpression",
},
},
Object {
"description": "param described with @argument tag",
"name": "test",
"title": "argument",
"type": Object {
"name": "string",
"type": "NameExpression",
},
},
],
"returns": Object {
"description": "test",
"title": "returns",
"type": Object {
"name": "string",
"type": "NameExpression",
},
},
"tags": Object {
"public": Array [
Object {
"description": null,
"title": "public",
"type": null,
},
],
},
},
]
`;
exports[`should highlight code in description (fenced code block) 1`] = `
Object {
"description": "The only true button.
\`\`\`js
alert ( 'Hello world' ) ;
\`\`\`
",
"displayName": "",
"doclets": Object {},
"methods": Array [],
"tags": Object {},
}
`;
exports[`should not crash when using doctrine to parse a default prop that isn't in the props list 1`] = `
Object {
"description": "The only true button.
",
"displayName": "",
"doclets": Object {},
"methods": Array [],
"props": Object {
"crash": Object {
"description": "",
"tags": Object {},
},
},
"tags": Object {},
}
`;
exports[`should remove non-public methods 1`] = `
Object {
"displayName": "Button",
"doclets": Object {},
"methods": Array [
Object {
"docblock": "Public method.
@public",
"tags": Object {
"public": Array [
Object {
"description": null,
"title": "public",
},
],
},
},
],
}
`;
exports[`should return an object for props 1`] = `
Object {
"description": "The only true button.
",
"displayName": "Button",
"doclets": Object {},
"methods": Array [],
"props": Object {
"children": Object {
"description": "Button label.",
"name": "children",
"required": true,
"tags": Object {},
"type": Object {
"name": "object",
},
},
"color": Object {
"description": "",
"name": "color",
"required": false,
"tags": Object {},
"type": Object {
"name": "string",
},
},
},
"tags": Object {},
}
`;
exports[`should return an object for props with doclets 1`] = `
Object {
"description": "The only true button.
",
"displayName": "Button",
"doclets": Object {
"bar": "Bar
",
"foo": "Foo",
},
"methods": Array [],
"tags": Object {
"bar": Array [
Object {
"description": "Bar",
"title": "bar",
},
],
"foo": Array [
Object {
"description": "Foo",
"title": "foo",
},
],
},
}
`;
exports[`should return an object for props without description 1`] = `
Object {
"displayName": "Button",
"doclets": Object {},
"methods": Array [],
"props": Object {
"children": Object {
"description": "Button label.",
"name": "children",
"required": true,
"tags": Object {},
"type": Object {
"name": "object",
},
},
},
}
`;
exports[`should return require statement for @example doclet 1`] = `
Object {
"description": "The only true button.
",
"displayName": "Button",
"doclets": Object {
"example": "../../../test/components/Placeholder/examples.md
",
},
"methods": Array [],
"tags": Object {
"example": Array [
Object {
"description": "../../../test/components/Placeholder/examples.md",
"title": "example",
},
],
},
}
`;
exports[`should return require statement for @example doclet only when the file exists 1`] = `
Object {
"description": "The only true button.
",
"displayName": "Button",
"doclets": Object {
"example": "example.md
",
},
"methods": Array [],
"tags": Object {
"example": Array [
Object {
"description": "example.md",
"title": "example",
},
],
},
}
`;
================================================
FILE: src/loaders/utils/__tests__/__snapshots__/getSections.spec.ts.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`getSections() should return an array 1`] = `
Array [
Object {
"components": Array [],
"content": Object {
"require": "!!~/src/loaders/examples-loader.js!~/test/components/Button/Readme.md",
},
"exampleMode": "collapse",
"href": undefined,
"name": "Readme",
"sectionDepth": 0,
"sections": Array [],
"slug": "section-readme-1",
"usageMode": "collapse",
},
Object {
"components": Array [
Object {
"filepath": "components/Annotation/Annotation.js",
"hasExamples": true,
"metadata": Object {},
"module": Object {
"require": "~/test/components/Annotation/Annotation.js",
},
"pathLine": "components/Annotation/Annotation.js",
"props": Object {
"require": "!!~/src/loaders/props-loader.js!~/test/components/Annotation/Annotation.js",
},
"slug": "annotation-1",
},
Object {
"filepath": "components/Button/Button.js",
"hasExamples": true,
"metadata": Object {},
"module": Object {
"require": "~/test/components/Button/Button.js",
},
"pathLine": "components/Button/Button.js",
"props": Object {
"require": "!!~/src/loaders/props-loader.js!~/test/components/Button/Button.js",
},
"slug": "button-2",
},
Object {
"filepath": "components/Placeholder/Placeholder.js",
"hasExamples": true,
"metadata": Object {
"require": "~/test/components/Placeholder/Placeholder.json",
},
"module": Object {
"require": "~/test/components/Placeholder/Placeholder.js",
},
"pathLine": "components/Placeholder/Placeholder.js",
"props": Object {
"require": "!!~/src/loaders/props-loader.js!~/test/components/Placeholder/Placeholder.js",
},
"slug": "placeholder-2",
},
Object {
"filepath": "components/Price/Price.js",
"hasExamples": true,
"metadata": Object {},
"module": Object {
"require": "~/test/components/Price/Price.js",
},
"pathLine": "components/Price/Price.js",
"props": Object {
"require": "!!~/src/loaders/props-loader.js!~/test/components/Price/Price.js",
},
"slug": "price-2",
},
Object {
"filepath": "components/RandomButton/RandomButton.js",
"hasExamples": true,
"metadata": Object {},
"module": Object {
"require": "~/test/components/RandomButton/RandomButton.js",
},
"pathLine": "components/RandomButton/RandomButton.js",
"props": Object {
"require": "!!~/src/loaders/props-loader.js!~/test/components/RandomButton/RandomButton.js",
},
"slug": "randombutton-2",
},
],
"content": undefined,
"exampleMode": "collapse",
"href": undefined,
"name": "Components",
"sectionDepth": 0,
"sections": Array [],
"slug": "section-components-1",
"usageMode": "collapse",
},
Object {
"components": Array [
Object {
"filepath": "components/Button/Button.js",
"hasExamples": true,
"metadata": Object {},
"module": Object {
"require": "~/test/components/Button/Button.js",
},
"pathLine": "components/Button/Button.js",
"props": Object {
"require": "!!~/src/loaders/props-loader.js!~/test/components/Button/Button.js",
},
"slug": "button-3",
},
Object {
"filepath": "components/Label/index.js",
"hasExamples": true,
"metadata": Object {},
"module": Object {
"require": "~/test/components/Label/index.js",
},
"pathLine": "components/Label/index.js",
"props": Object {
"require": "!!~/src/loaders/props-loader.js!~/test/components/Label/index.js",
},
"slug": "label-1",
},
Object {
"filepath": "components/Placeholder/Placeholder.js",
"hasExamples": true,
"metadata": Object {
"require": "~/test/components/Placeholder/Placeholder.json",
},
"module": Object {
"require": "~/test/components/Placeholder/Placeholder.js",
},
"pathLine": "components/Placeholder/Placeholder.js",
"props": Object {
"require": "!!~/src/loaders/props-loader.js!~/test/components/Placeholder/Placeholder.js",
},
"slug": "placeholder-3",
},
Object {
"filepath": "components/Price/Price.js",
"hasExamples": true,
"metadata": Object {},
"module": Object {
"require": "~/test/components/Price/Price.js",
},
"pathLine": "components/Price/Price.js",
"props": Object {
"require": "!!~/src/loaders/props-loader.js!~/test/components/Price/Price.js",
},
"slug": "price-3",
},
Object {
"filepath": "components/RandomButton/RandomButton.js",
"hasExamples": true,
"metadata": Object {},
"module": Object {
"require": "~/test/components/RandomButton/RandomButton.js",
},
"pathLine": "components/RandomButton/RandomButton.js",
"props": Object {
"require": "!!~/src/loaders/props-loader.js!~/test/components/RandomButton/RandomButton.js",
},
"slug": "randombutton-3",
},
],
"content": undefined,
"exampleMode": "collapse",
"href": undefined,
"ignore": "**/components/Annotation/*",
"name": "Ignore",
"sectionDepth": 0,
"sections": Array [],
"slug": "section-ignore-2",
"usageMode": "collapse",
},
Object {
"components": Array [],
"content": Object {
"content": "Hello World",
"type": "markdown",
},
"exampleMode": "collapse",
"href": undefined,
"name": "Ignore",
"sectionDepth": 0,
"sections": Array [],
"slug": "section-ignore-3",
"usageMode": "collapse",
},
]
`;
exports[`processSection() should return an object for section with components 1`] = `
Object {
"components": Array [
Object {
"filepath": "components/Annotation/Annotation.js",
"hasExamples": true,
"metadata": Object {},
"module": Object {
"require": "~/test/components/Annotation/Annotation.js",
},
"pathLine": "components/Annotation/Annotation.js",
"props": Object {
"require": "!!~/src/loaders/props-loader.js!~/test/components/Annotation/Annotation.js",
},
"slug": "annotation",
},
Object {
"filepath": "components/Button/Button.js",
"hasExamples": true,
"metadata": Object {},
"module": Object {
"require": "~/test/components/Button/Button.js",
},
"pathLine": "components/Button/Button.js",
"props": Object {
"require": "!!~/src/loaders/props-loader.js!~/test/components/Button/Button.js",
},
"slug": "button",
},
Object {
"filepath": "components/Placeholder/Placeholder.js",
"hasExamples": true,
"metadata": Object {
"require": "~/test/components/Placeholder/Placeholder.json",
},
"module": Object {
"require": "~/test/components/Placeholder/Placeholder.js",
},
"pathLine": "components/Placeholder/Placeholder.js",
"props": Object {
"require": "!!~/src/loaders/props-loader.js!~/test/components/Placeholder/Placeholder.js",
},
"slug": "placeholder",
},
Object {
"filepath": "components/Price/Price.js",
"hasExamples": true,
"metadata": Object {},
"module": Object {
"require": "~/test/components/Price/Price.js",
},
"pathLine": "components/Price/Price.js",
"props": Object {
"require": "!!~/src/loaders/props-loader.js!~/test/components/Price/Price.js",
},
"slug": "price",
},
Object {
"filepath": "components/RandomButton/RandomButton.js",
"hasExamples": true,
"metadata": Object {},
"module": Object {
"require": "~/test/components/RandomButton/RandomButton.js",
},
"pathLine": "components/RandomButton/RandomButton.js",
"props": Object {
"require": "!!~/src/loaders/props-loader.js!~/test/components/RandomButton/RandomButton.js",
},
"slug": "randombutton",
},
],
"content": undefined,
"exampleMode": "collapse",
"href": undefined,
"name": "Components",
"sectionDepth": 0,
"sections": Array [],
"slug": "section-components",
"usageMode": "collapse",
}
`;
exports[`processSection() should return an object for section with content 1`] = `
Object {
"components": Array [],
"content": Object {
"require": "!!~/src/loaders/examples-loader.js!~/test/components/Button/Readme.md",
},
"exampleMode": "collapse",
"href": undefined,
"name": "Readme",
"sectionDepth": 0,
"sections": Array [],
"slug": "section-readme",
"usageMode": "collapse",
}
`;
exports[`processSection() should return an object for section with content as function 1`] = `
Object {
"components": Array [],
"content": Object {
"content": "Hello World",
"type": "markdown",
},
"exampleMode": "collapse",
"href": undefined,
"name": "Ignore",
"sectionDepth": 0,
"sections": Array [],
"slug": "section-ignore-1",
"usageMode": "collapse",
}
`;
exports[`processSection() should return an object for section without ignored components 1`] = `
Object {
"components": Array [
Object {
"filepath": "components/Button/Button.js",
"hasExamples": true,
"metadata": Object {},
"module": Object {
"require": "~/test/components/Button/Button.js",
},
"pathLine": "components/Button/Button.js",
"props": Object {
"require": "!!~/src/loaders/props-loader.js!~/test/components/Button/Button.js",
},
"slug": "button-1",
},
Object {
"filepath": "components/Label/index.js",
"hasExamples": true,
"metadata": Object {},
"module": Object {
"require": "~/test/components/Label/index.js",
},
"pathLine": "components/Label/index.js",
"props": Object {
"require": "!!~/src/loaders/props-loader.js!~/test/components/Label/index.js",
},
"slug": "label",
},
Object {
"filepath": "components/Placeholder/Placeholder.js",
"hasExamples": true,
"metadata": Object {
"require": "~/test/components/Placeholder/Placeholder.json",
},
"module": Object {
"require": "~/test/components/Placeholder/Placeholder.js",
},
"pathLine": "components/Placeholder/Placeholder.js",
"props": Object {
"require": "!!~/src/loaders/props-loader.js!~/test/components/Placeholder/Placeholder.js",
},
"slug": "placeholder-1",
},
Object {
"filepath": "components/Price/Price.js",
"hasExamples": true,
"metadata": Object {},
"module": Object {
"require": "~/test/components/Price/Price.js",
},
"pathLine": "components/Price/Price.js",
"props": Object {
"require": "!!~/src/loaders/props-loader.js!~/test/components/Price/Price.js",
},
"slug": "price-1",
},
Object {
"filepath": "components/RandomButton/RandomButton.js",
"hasExamples": true,
"metadata": Object {},
"module": Object {
"require": "~/test/components/RandomButton/RandomButton.js",
},
"pathLine": "components/RandomButton/RandomButton.js",
"props": Object {
"require": "!!~/src/loaders/props-loader.js!~/test/components/RandomButton/RandomButton.js",
},
"slug": "randombutton-1",
},
],
"content": undefined,
"exampleMode": "collapse",
"href": undefined,
"ignore": "**/components/Annotation/*",
"name": "Ignore",
"sectionDepth": 0,
"sections": Array [],
"slug": "section-ignore",
"usageMode": "collapse",
}
`;
================================================
FILE: src/loaders/utils/__tests__/__snapshots__/highlightCode.spec.ts.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should highlight code with specified language 1`] = `"< p> Hello React</ p> "`;
================================================
FILE: src/loaders/utils/__tests__/__snapshots__/highlightCodeInMarkdown.spec.ts.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should highlight code with specified language 1`] = `
"The only true button.
\`\`\`html
< p> Hello React</ p>
\`\`\`
"
`;
exports[`should not highlight code without language 1`] = `
"The only \`true\` button.
Hello React
"
`;
================================================
FILE: src/loaders/utils/__tests__/__snapshots__/processComponent.spec.ts.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`processComponent() should return an object for section with content 1`] = `
Object {
"filepath": "../../../../pizza.js",
"hasExamples": true,
"metadata": Object {},
"module": Object {
"require": "pizza.js",
},
"pathLine": "../../../../pizza.js",
"props": Object {
"require": "!!~/src/loaders/props-loader.js!pizza.js",
},
"slug": "pizza",
}
`;
================================================
FILE: src/loaders/utils/__tests__/chunkify.spec.ts
================================================
import chunkify from '../chunkify';
import * as Rsg from '../../../typings';
/* eslint-disable max-len */
it('should separate Markdown and component examples', () => {
const markdown = `
# Header
Text with *some* **formatting** and a [link](/foo).
And some HTML.

This code example should be rendered as a playground:
Hello Markdown!
Text with some \`code\` (playground too).
\`\`\`
Hello Markdown!
\`\`\`
And some language and modifier (playground again):
\`\`\`jsx noeditor
Hello Markdown!
\`\`\`
This should be just highlighted:
\`\`\`jsx static
Hello Markdown!
\`\`\`
This should be highlighted too:
\`\`\`html
Hello Markdown!
\`\`\`
`;
const actual = chunkify(markdown);
expect(actual).toMatchSnapshot();
});
it('should render some extensions as a playground', () => {
const markdown = `
This below extensions should be rendered as a playground:
\`\`\`javascript
Hello javascript playground!
\`\`\`
\`\`\`js
Hello js playground!
\`\`\`
\`\`\`jsx
Hello jsx playground!
\`\`\`
\`\`\`typescript
Hello typescript playground!
\`\`\`
\`\`\`ts
Hello ts playground!
\`\`\`
\`\`\`tsx
Hello tsx playground!
\`\`\`
`;
const actual = chunkify(markdown);
expect(actual.slice(1).every((chunk) => chunk.type === 'code')).toBe(true);
});
it('should not add empty Markdown chunks', () => {
const markdown = `
Foo:
Hello Markdown!
`;
const expected = [
{
type: 'markdown',
content: 'Foo:',
},
{
type: 'code',
content: 'Hello Markdown! ',
settings: {},
},
];
const actual = chunkify(markdown);
expect(actual).toEqual(expected);
});
it('should parse examples settings correctly', () => {
const markdown = `
Pass props to CodeRenderer
\`\`\`js { "showCode": true }
Hello Markdown!
\`\`\`
\`\`\`js { "frame": {"width": "400px"} }
Example in frame and Without editor
\`\`\`
Pass props to PreviewRenderer
\`\`\`jsx { "noEditor": true }
Hello Markdown!
\`\`\`
\`\`\`jsx static
This is Highlighted!
\`\`\`
`;
const actual = chunkify(markdown);
expect(actual).toMatchSnapshot();
});
it('should call updateExample function for example', () => {
const markdown = `
\`\`\`jsx {"file": "./src/button/example.jsx"}
\`\`\`
`;
const expected = [
{
type: 'code',
content: 'Hello Markdown! ',
settings: {},
},
];
const updateExample = (props: Omit): Omit => {
const content = props.content;
const lang = props.lang;
const settings = props.settings;
if (settings && typeof settings.file === 'string') {
delete settings.file;
return {
content: 'Hello Markdown! ',
settings,
lang,
};
}
return {
content,
settings,
lang,
};
};
const actual = chunkify(markdown, updateExample);
expect(actual).toEqual(expected);
});
it('should even parse examples with custom extensions', () => {
const markdown = `
Custom extensions
\`\`\`vue
Example in vue
\`\`\`
`;
const actual = chunkify(markdown, undefined, ['vue']);
expect(actual).toMatchSnapshot();
});
it('should parse undefined custom extensions without throwing', () => {
const markdown = `
Undefined extensions (default)
\`\`\`jsx
Example in jsx with undefined extensions
\`\`\`
\`\`\`pizza
Example in pizza with undefined extensions (test double)
\`\`\`
`;
const actual = chunkify(markdown, undefined, undefined);
expect(actual).toMatchSnapshot();
});
================================================
FILE: src/loaders/utils/__tests__/expandDefaultComponent.spec.ts
================================================
import expandDefaultComponent from '../expandDefaultComponent';
it('expandDefaultComponent() replace placeholders with component name', () => {
const exampleMarkdown = `
<__COMPONENT__>
text
Name of component: __COMPONENT__
<__COMPONENT__ />
`;
const result = expandDefaultComponent(exampleMarkdown, 'FooComponent');
expect(result).not.toMatch(/__COMPONENT__/);
expect(result).toMatch(/FooComponent/);
expect((result.match(/FooComponent/g) || '').length).toBe(4);
});
================================================
FILE: src/loaders/utils/__tests__/filterComponentsWithExample.spec.ts
================================================
import filterComponentsWithExample from '../filterComponentsWithExample';
const sections = [
{
name: 'Readme',
content: 'Readme.md',
components: [],
sections: [],
},
{
name: 'Components',
components: [
{
filepath: 'components/Button/Button.js',
hasExamples: 'require()',
},
{
filepath: 'components/Icon/Icon.js',
},
],
sections: [],
},
{
name: 'Nesting',
components: [],
sections: [
{
name: 'Nested',
components: [
{
filepath: 'components/Image/Image.js',
},
{
filepath: 'components/Modal/Modal.js',
hasExamples: 'require()',
},
],
sections: [],
},
{
name: 'Nested 2',
components: [
{
filepath: 'components/Avatar/Avatar.js',
},
],
sections: [],
},
],
},
] as any;
it('should skip components without example file', () => {
const result = filterComponentsWithExample(sections);
expect(result).toMatchSnapshot();
});
================================================
FILE: src/loaders/utils/__tests__/getAllContentPages.spec.ts
================================================
import getAllContentPages from '../getAllContentPages';
import * as Rsg from '../../../typings';
const readmeContent: Rsg.MarkdownExample = {
type: 'markdown',
content: '# Readme',
};
const nestedContent: Rsg.MarkdownExample = {
type: 'markdown',
content: '# Nested',
};
const sections: Rsg.LoaderSection[] = [
{
name: 'Readme',
content: readmeContent,
components: [],
sections: [],
},
{
name: 'Components',
components: [],
sections: [],
},
{
name: 'Nesting',
components: [],
sections: [
{
name: 'Nested',
components: [],
sections: [],
},
{
name: 'Nested 2',
content: nestedContent,
components: [],
sections: [],
},
],
},
];
it('should return all content pages', () => {
const result = getAllContentPages(sections);
expect(result).toEqual([readmeContent, nestedContent]);
});
================================================
FILE: src/loaders/utils/__tests__/getAst.spec.ts
================================================
import acornJsx from 'acorn-jsx';
import getAst from '../getAst';
describe('getAst', () => {
test('return AST', () => {
const result = getAst(`42`);
expect(result).toMatchSnapshot();
});
test('accept Acorn plugins', () => {
const result = getAst(` `, [acornJsx()]);
expect(result).toMatchSnapshot();
});
});
================================================
FILE: src/loaders/utils/__tests__/getComponentFiles.spec.ts
================================================
import path from 'path';
import deabsDeep from 'deabsdeep';
import getComponentFiles from '../getComponentFiles';
const configDir = path.resolve(__dirname, '../../../../test');
const components = ['components/Annotation/Annotation.js', 'components/Button/Button.js'];
const processedComponents = components.map(c => `~/${c}`);
const glob = 'components/**/[A-Z]*.js';
const globArray = ['components/Annotation/[A-Z]*.js', 'components/Button/[A-Z]*.js'];
const deabs = (x: string[]) => deabsDeep(x, { root: configDir });
it('getComponentFiles() should return an empty array if components is null', () => {
const result = getComponentFiles();
expect(result).toEqual([]);
});
it('getComponentFiles() should accept components as a function that returns file names', () => {
const result = getComponentFiles(() => components, configDir);
expect(deabs(result)).toEqual(processedComponents);
});
it('getComponentFiles() should accept components as a function that returns absolute paths', () => {
const absolutize = (files: string[]) => files.map(file => path.join(configDir, file));
const result = getComponentFiles(() => absolutize(components), configDir);
expect(deabs(result)).toEqual(processedComponents);
});
it('getComponentFiles() should accept components as a function that returns globs', () => {
const result = getComponentFiles(() => globArray, configDir);
expect(deabs(result)).toEqual([
'~/components/Annotation/Annotation.js',
'~/components/Button/Button.js',
]);
});
it('getComponentFiles() should accept components as an array of file names', () => {
const result = getComponentFiles(components, configDir);
expect(deabs(result)).toEqual(processedComponents);
});
it('getComponentFiles() should accept components as an array of absolute paths', () => {
const absolutize = (files: string[]) => files.map(file => path.join(configDir, file));
const result = getComponentFiles(absolutize(components), configDir);
expect(deabs(result)).toEqual(processedComponents);
});
it('getComponentFiles() should accept components as an array of globs', () => {
const result = getComponentFiles(globArray, configDir);
expect(deabs(result)).toEqual([
'~/components/Annotation/Annotation.js',
'~/components/Button/Button.js',
]);
});
it('getComponentFiles() should accept components as a glob', () => {
const result = getComponentFiles(glob, configDir);
expect(deabs(result)).toEqual([
'~/components/Annotation/Annotation.js',
'~/components/Button/Button.js',
'~/components/Placeholder/Placeholder.js',
'~/components/Price/Price.js',
'~/components/RandomButton/RandomButton.js',
]);
});
it('getComponentFiles() should ignore specified patterns for globs', () => {
const result = getComponentFiles(glob, configDir, ['**/*Button*']);
expect(deabs(result)).toEqual([
'~/components/Annotation/Annotation.js',
'~/components/Placeholder/Placeholder.js',
'~/components/Price/Price.js',
]);
});
it('getComponentFiles() should ignore specified patterns for globs in arrays', () => {
const result = getComponentFiles(globArray, configDir, ['**/*Button*']);
expect(deabs(result)).toEqual(['~/components/Annotation/Annotation.js']);
});
it('getComponentFiles() should ignore specified patterns for globs from functions', () => {
const result = getComponentFiles(() => globArray, configDir, ['**/*Button*']);
expect(deabs(result)).toEqual(['~/components/Annotation/Annotation.js']);
});
it('getComponentFiles() should throw if components is not a function, array or a string', () => {
const fn = () => getComponentFiles(42 as any, configDir);
expect(fn).toThrowError('should be string, function or array');
});
================================================
FILE: src/loaders/utils/__tests__/getComponentFilesFromSections.spec.ts
================================================
import path from 'path';
import deabsDeep from 'deabsdeep';
import getComponentFilesFromSections from '../getComponentFilesFromSections';
const configDir = path.resolve(__dirname, '../../../../test');
const sections = [
{
name: 'Readme',
content: 'Readme.md',
},
{
name: 'Components',
components: 'components/**/B*.js',
},
{
name: 'Nesting',
sections: [
{
name: 'Nested',
components: 'components/**/P*.js',
},
],
},
];
const deabs = (x: string[]) => deabsDeep(x, { root: configDir });
it('getComponentFilesFromSections() should return a list of files', () => {
const result = getComponentFilesFromSections(sections, configDir);
expect(deabs(result)).toEqual([
'~/components/Button/Button.js',
'~/components/Placeholder/Placeholder.js',
'~/components/Price/Price.js',
]);
});
it('getComponentFilesFromSections() should ignore specified patterns', () => {
const result = getComponentFilesFromSections(sections, configDir, ['**/*Button*']);
expect(deabs(result)).toEqual([
'~/components/Placeholder/Placeholder.js',
'~/components/Price/Price.js',
]);
});
================================================
FILE: src/loaders/utils/__tests__/getComponentPatternsFromSections.spec.ts
================================================
import getComponentPatternsFromSections from '../getComponentPatternsFromSections';
const sections = [
{
name: 'Readme',
content: 'Readme.md',
},
{
name: 'Components',
components: ['components/**/B*.js'],
},
{
name: 'Nesting',
sections: [
{
name: 'Nested',
components: ['components/**/P*.js'],
},
],
},
{
name: 'Nesting With Components',
components: ['components/**/T*.js'],
// is this on purpose or a bug ?
// a section cannot conatin `components` and nested `sections`
sections: [
{
name: 'Ignored Nested',
components: ['components/**/O*.js'],
},
],
},
];
it('should return a list of patterns', () => {
const result = getComponentPatternsFromSections(sections);
expect(result).toEqual(['components/**/B*.js', 'components/**/P*.js', 'components/**/T*.js']);
});
================================================
FILE: src/loaders/utils/__tests__/getComponents.spec.ts
================================================
import path from 'path';
import identity from 'lodash/identity';
import getComponents from '../getComponents';
it('getComponents() should return an object for components', () => {
const result = getComponents(['Foo.js', 'Bar.js'], {
configDir: path.resolve(__dirname, '../../../test'),
getExampleFilename: identity,
getComponentPathLine: identity,
} as any);
expect(result).toMatchSnapshot();
});
================================================
FILE: src/loaders/utils/__tests__/getExamples.spec.ts
================================================
import deabsDeep from 'deabsdeep';
import { vol } from 'memfs';
import getExamples from '../getExamples';
jest.mock('fs', () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
return require('memfs').fs;
});
const file = '../pizza.js';
const displayName = 'Pizza';
const examplesFile = './Pizza.md';
const defaultExample = './Default.md';
afterEach(() => {
vol.reset();
});
test('require an example file if component has example file', () => {
vol.fromJSON({ [examplesFile]: 'pizza' });
const result = getExamples(file, displayName, examplesFile);
expect(result && deabsDeep(result).require).toMatchInlineSnapshot(
`"!!~/src/loaders/examples-loader.js?displayName=Pizza&file=.%2F..%2Fpizza.js&shouldShowDefaultExample=false!./Pizza.md"`
);
});
test('require default example file if component has no example in the file system', () => {
const result = getExamples(file, displayName, examplesFile, defaultExample);
expect(result && deabsDeep(result).require).toMatchInlineSnapshot(
`"!!~/src/loaders/examples-loader.js?displayName=Pizza&file=.%2F..%2Fpizza.js&shouldShowDefaultExample=false!./Default.md"`
);
});
test('require default example has no example file', () => {
const result = getExamples(file, displayName, false, defaultExample);
expect(result && deabsDeep(result).require).toMatchInlineSnapshot(
`"!!~/src/loaders/examples-loader.js?displayName=Pizza&file=.%2F..%2Fpizza.js&shouldShowDefaultExample=true!./Default.md"`
);
});
test('return null if component has no example file or default example', () => {
const result = getExamples(file, displayName);
expect(result).toEqual(null);
});
================================================
FILE: src/loaders/utils/__tests__/getImports.spec.ts
================================================
import getImports from '../getImports';
test('find calls to require() in code', () => {
expect(getImports(`require('foo')`)).toEqual(['foo']);
expect(getImports(`require('./foo')`)).toEqual(['./foo']);
expect(getImports(`require('foo');require('bar')`)).toEqual(['foo', 'bar']);
});
test('find import statements in code', () => {
expect(getImports(`import A from 'pizza';`)).toEqual(['pizza']);
expect(getImports(`import A from './pizza';`)).toEqual(['./pizza']);
expect(getImports(`import { A as X, B } from 'lunch';`)).toEqual(['lunch']);
expect(getImports(`import A, { B as X, C } from 'lunch';`)).toEqual(['lunch']);
expect(getImports(`import A from 'foo';import B from 'bar';`)).toEqual(['foo', 'bar']);
});
test('work with JSX', () => {
expect(getImports(`const A = require('pizza'); `)).toEqual(['pizza']);
expect(getImports(`import A from 'pizza';foo `)).toEqual(['pizza']);
});
test('allow comments', () => {
expect(
getImports(`
/**
* Some important comment
*/
import A from 'dog'
/* Less important comments */
import B from 'cat'
// Absolutely not important comment
import C from 'capybara'
import D from 'hamster' // One more comment
import E from 'snake'
`)
).toEqual(['dog', 'cat', 'capybara', 'hamster', 'snake']);
});
test('ignore dynamic requires', () => {
expect(getImports(`require('foo' + 'bar')`)).toEqual([]);
});
test('ignore imports in comments', () => {
expect(
getImports(`
import A from 'pizza'
// import one from 'one';
/** import two from 'two' */
/* import three from 'three' */
/*
import four from 'four';
import five from 'five';
*/
`)
).toEqual(['pizza']);
});
test('ignore imports in strings', () => {
expect(
getImports(`
import A from 'pizza'
const foo = "import foo from 'foo'"
const bar = 'import bar from "bar"'
const baz = \`import baz from 'baz'\`
`)
).toEqual(['pizza']);
});
test('ignore imports in JSX', () => {
expect(
getImports(`
import A from 'pizza';
import foo from 'foo'
`)
).toEqual(['pizza']);
});
test('ignore multiple root JSX elements', () => {
expect(getImports(` `)).toEqual([]);
});
test('ignore syntax errors', () => {
expect(getImports(`*`)).toEqual([]);
});
================================================
FILE: src/loaders/utils/__tests__/getNameFromFilePath.spec.ts
================================================
import path from 'path';
import getNameFromFilePath from '../getNameFromFilePath';
it('should return the file name without extension', () => {
expect(
getNameFromFilePath(path.join('an', 'absolute', 'path', 'to', 'YourComponent.js'))
).toEqual('YourComponent');
});
it('should use the directory name if the file name is index.js', () => {
expect(
getNameFromFilePath(path.join('an', 'absolute', 'path', 'to', 'YourComponent', 'index.js'))
).toEqual('YourComponent');
});
it('should capitalize the display name', () => {
expect(
getNameFromFilePath(path.join('an', 'absolute', 'path', 'to', 'yourComponent.js'))
).toEqual('YourComponent');
expect(
getNameFromFilePath(path.join('an', 'absolute', 'path', 'to', 'your-component', 'index.js'))
).toEqual('YourComponent');
expect(
getNameFromFilePath(path.join('an', 'absolute', 'path', 'to', 'yourButtonTS.tsx'))
).toEqual('YourButtonTS');
expect(
getNameFromFilePath(path.join('an', 'absolute', 'path', 'to', 'your-buttonTS', 'index.tsx'))
).toEqual('YourButtonTS');
expect(
getNameFromFilePath(path.join('an', 'absolute', 'path', 'to', 'your_button--TS', 'index.tsx'))
).toEqual('YourButtonTS');
expect(
getNameFromFilePath(path.join('an', 'absolute', 'path', 'to', 'ButtonTS', 'index.tsx'))
).toEqual('ButtonTS');
});
================================================
FILE: src/loaders/utils/__tests__/getProps.spec.ts
================================================
import path from 'path';
import getProps from '../getProps';
it('should return an object for props', () => {
const result = getProps({
displayName: 'Button',
description: 'The only true button.',
methods: [],
props: {
children: {
name: 'children',
type: {
name: 'object',
},
required: true,
description: 'Button label.',
},
color: {
name: 'color',
type: {
name: 'string',
},
required: false,
description: '',
},
},
});
expect(result).toMatchSnapshot();
});
it('should return an object for props without description', () => {
const result = getProps({
displayName: 'Button',
props: {
children: {
name: 'children',
type: {
name: 'object',
},
required: true,
description: 'Button label.',
},
},
});
expect(result).toMatchSnapshot();
});
it('should remove non-public methods', () => {
const result = getProps(
{
displayName: 'Button',
methods: [
{
docblock: `Public method.
@public`,
},
{
docblock: `Private method.
@private`,
},
{
docblock: 'Private method by default.',
},
] as any,
},
__filename
);
expect(result).toMatchSnapshot();
});
it('should get method info from docblock and merge it', () => {
const result = getProps(
{
displayName: 'Button',
methods: [
{
docblock: `
Baz method with foo param
@public
@returns {string} test
`,
},
] as any,
},
__filename
);
expect(result.methods).toMatchSnapshot();
});
it('should accept @return as a synonym of @returns', () => {
const result = getProps(
{
displayName: 'Button',
methods: [
{
docblock: `
Baz method with foo param
@public
@return {string} test
`,
},
] as any,
},
__filename
);
expect(result.methods).toMatchSnapshot();
});
it('should get method params info from docblock and merge it with passed method info', () => {
const result = getProps(
{
displayName: 'Button',
methods: [
{
docblock: `
Foo method with baz param
@public
@param {string} [baz=bar]
@arg {string} foo param described with @arg tag
@argument {string} test param described with @argument tag
@returns {string} test
`,
params: [
{
name: 'baz',
},
{
name: 'foo',
},
{
name: 'test',
},
],
},
] as any,
},
__filename
);
expect(result.methods).toMatchSnapshot();
});
it('should return an object for props with doclets', () => {
const result = getProps(
{
displayName: 'Button',
description: `
The only true button.
@foo Foo
@bar Bar
`,
},
__filename
);
expect(result).toMatchSnapshot();
});
it('should return require statement for @example doclet', () => {
const result = getProps(
{
displayName: 'Button',
description: `
The only true button.
@example ../../../test/components/Placeholder/examples.md
`,
},
__filename
);
expect(result).toMatchSnapshot();
});
it('should return require statement for @example doclet only when the file exists', () => {
const result = getProps(
{
displayName: 'Button',
description: `
The only true button.
@example example.md
`,
},
__filename
);
expect(result).toMatchSnapshot();
});
it('should highlight code in description (fenced code block)', () => {
const result = getProps({
description: `
The only true button.
\`\`\`js
alert('Hello world');
\`\`\`
`,
});
expect(result).toMatchSnapshot();
});
it("should not crash when using doctrine to parse a default prop that isn't in the props list", () => {
const result = getProps({
description: 'The only true button.',
methods: [],
props: {
crash: {
description: undefined,
},
} as any,
});
expect(result).toMatchSnapshot();
});
it('should not crash when using doctrine to parse a return method that does not have type in it', () => {
const result = getProps(
{
displayName: 'Button',
methods: [
{
docblock: `
Public Method
@public
@returns {Boolean} return a Boolean Value
`,
returns: {
description: 'return a Boolean Value',
type: { name: 'boolean' },
},
},
] as any,
},
__filename
);
// @ts-ignore
expect(result.methods[0].returns).toEqual(
expect.objectContaining({
description: 'return a Boolean Value',
type: {
name: 'boolean',
type: 'NameExpression',
},
})
);
});
it('should guess a displayName for components that react-docgen was not able to recognize', () => {
const result = getProps(
{
methods: [],
props: {},
},
path.join('an', 'absolute', 'path', 'to', 'YourComponent.js')
);
expect(result).toHaveProperty('displayName', 'YourComponent');
});
describe('with @visibleName tag present in the description', () => {
const result = getProps({
description: 'bar\n@visibleName foo',
});
it('should set visibleName property on the docs object', () => {
expect(result).toHaveProperty('visibleName', 'foo');
});
it('should delete visibleName from doclets on the docs object', () => {
expect(result.doclets).not.toHaveProperty('visibleName');
});
it('should delete visibleName from tags on the docs object', () => {
expect(result.tags).not.toHaveProperty('visibleName');
});
});
================================================
FILE: src/loaders/utils/__tests__/getSections.spec.ts
================================================
import path from 'path';
import getSections, { processSection } from '../getSections';
import * as Rsg from '../../../typings';
const configDir = path.resolve(__dirname, '../../../../test');
const config = {
configDir,
exampleMode: 'collapse',
usageMode: 'collapse',
getExampleFilename: (a: string) => a,
getComponentPathLine: (a: string) => a,
} as Rsg.SanitizedStyleguidistConfig;
const sections: Rsg.ConfigSection[] = [
{
name: 'Readme',
content: 'components/Button/Readme.md',
},
{
name: 'Components',
components: 'components/**/[A-Z]*.js',
},
{
name: 'Ignore',
components: 'components/**/*.js',
ignore: '**/components/Annotation/*',
},
{
name: 'Ignore',
content: () => 'Hello World',
} as any,
];
const sectionsWithDepth = [
{
name: 'Documentation',
sections: [
{
name: 'Files',
sections: [
{
name: 'First File',
},
],
},
],
sectionDepth: 2,
},
{
name: 'Components',
expand: true,
sections: [
{
name: 'Buttons',
},
],
sectionDepth: 0,
},
];
const sectionsWithBadDepth = [
{
name: 'Documentation',
sections: [
{
name: 'Files',
sections: [
{
name: 'First File',
},
],
sectionDepth: 2,
},
],
},
];
function filterSectionDepth(section: Rsg.LoaderSection): Rsg.ConfigSection {
if (section.sections && section.sections.length) {
return {
sectionDepth: section.sectionDepth,
sections: section.sections.map(filterSectionDepth),
};
}
return {
sectionDepth: section.sectionDepth,
};
}
it('processSection() should return an object for section with content', () => {
const result = processSection(sections[0], config);
expect(result).toMatchSnapshot();
});
it('processSection() should throw when content file not found', () => {
const fn = () => processSection({ content: 'pizza' }, config);
expect(fn).toThrowError('Section content file not found');
});
it('processSection() should return an object for section with components', () => {
const result = processSection(sections[1], config);
expect(result).toMatchSnapshot();
});
it('processSection() should return an object for section without ignored components', () => {
const result = processSection(sections[2], config);
expect(result).toMatchSnapshot();
});
it('processSection() should return an object for section with content as function', () => {
const result = processSection(sections[3], config);
expect(result).toMatchSnapshot();
});
it('getSections() should return an array', () => {
const result = getSections(sections, config);
expect(result).toMatchSnapshot();
});
it('getSections() should return an array of sectionsWithDepth with sectionDepth decreasing', () => {
const result = getSections(sectionsWithDepth, config);
expect(result.map(filterSectionDepth)).toEqual([
{
sectionDepth: 2,
sections: [
{
sectionDepth: 1,
sections: [
{
sectionDepth: 0,
},
],
},
],
},
{
sectionDepth: 0,
sections: [
{
sectionDepth: 0,
},
],
},
]);
});
it('getSections() should make custom options by user available', () => {
const result = getSections(sectionsWithDepth, config);
const expandSection = result.find(section => section.name === 'Components');
expect(expandSection).toHaveProperty('expand');
});
it('getSections() should return an array of sectionsWithBadDepth taking the sectionDepth of the first depth of the sections', () => {
const result = getSections(sectionsWithBadDepth, config);
expect(result.map(filterSectionDepth)).toEqual([
{
sectionDepth: 0,
sections: [
{
sectionDepth: 0,
sections: [
{
sectionDepth: 0,
},
],
},
],
},
]);
});
================================================
FILE: src/loaders/utils/__tests__/highlightCode.spec.ts
================================================
import glogg from 'glogg';
import highlightCode from '../highlightCode';
const logger = glogg('rsg');
const code = 'Hello React
';
it('should highlight code with specified language', () => {
const actual = highlightCode(code, 'html');
expect(actual).toMatchSnapshot();
});
it('should warn when language not found', () => {
const warn = jest.fn();
logger.once('warn', warn);
const actual = highlightCode(code, 'pizza');
expect(actual).toBe(code);
expect(warn).toBeCalledWith(
'Syntax highlighting for “pizza” isn’t supported. Supported languages are: markup, html, mathml, svg, xml, ssml, atom, rss, css, clike, javascript, js, markdown, md, scss, less, flow, typescript, ts, jsx, tsx, graphql, json, webmanifest, bash, shell, diff.'
);
});
it('should not highlight code without language', () => {
const actual = highlightCode(code);
expect(actual).toBe(code);
});
================================================
FILE: src/loaders/utils/__tests__/highlightCodeInMarkdown.spec.ts
================================================
import highlightCodeInMarkdown from '../highlightCodeInMarkdown';
it('should highlight code with specified language', () => {
const text = `
The only true button.
\`\`\`html
Hello React
\`\`\`
`;
const actual = highlightCodeInMarkdown(text);
expect(actual).toMatchSnapshot();
});
it('should not highlight code without language', () => {
const text = `
The only \`true\` button.
\`\`\`
Hello React
\`\`\`
`;
const actual = highlightCodeInMarkdown(text);
expect(actual).toMatchSnapshot();
});
================================================
FILE: src/loaders/utils/__tests__/parseExample.spec.ts
================================================
import parseExample from '../parseExample';
const content = 'Hello Markdown! ';
it('should parse modifiers as JSON', () => {
const actual = parseExample(content, 'js', '{ "showcode": true }');
expect(actual).toEqual({
lang: 'js',
settings: { showcode: true },
content,
});
});
it('should lowercase JSON keys', () => {
const actual = parseExample(content, 'js', '{ "showCode": true }');
expect(actual).toEqual({
lang: 'js',
settings: { showcode: true },
content,
});
});
it('should parse modifiers as a space-separated string', () => {
const actual = parseExample(content, 'jsx', 'showcode static');
expect(actual).toEqual({
lang: 'jsx',
settings: { showcode: true, static: true },
content,
});
});
it('should lowercase modifiers', () => {
const actual = parseExample(content, 'jsx', 'showCode Static');
expect(actual).toEqual({
lang: 'jsx',
settings: { showcode: true, static: true },
content,
});
});
it('should return settings as an empty object', () => {
const actual = parseExample(content, 'js');
expect(actual).toEqual({
lang: 'js',
settings: {},
content,
});
});
it('should accept language as null', () => {
const actual = parseExample(content, null);
expect(actual).toEqual({
lang: null,
settings: {},
content,
});
});
it('should apply an update function', () => {
const actual = parseExample(content, 'js', 'coffee', a => ({ ...a, lang: 'pizza' }));
expect(actual).toEqual({
lang: 'pizza',
settings: { coffee: true },
content,
});
});
it('should return an error when JSON is invalid', () => {
const actual = parseExample(content, 'js', '{ nope }');
expect(actual).toEqual({
error: expect.stringMatching('Cannot parse modifiers'),
});
});
================================================
FILE: src/loaders/utils/__tests__/processComponent.spec.ts
================================================
import path from 'path';
import processComponent from '../processComponent';
const config = {
configDir: __dirname,
getExampleFilename: (componentpath: string) =>
path.join(path.dirname(componentpath), 'Readme.md'),
getComponentPathLine: (componentpath: string) => componentpath,
};
it('processComponent() should return an object for section with content', () => {
const result = processComponent('pizza.js', config as any);
expect(result).toMatchSnapshot();
});
================================================
FILE: src/loaders/utils/__tests__/removeDoclets.spec.ts
================================================
import removeDoclets from '../removeDoclets';
/* eslint-disable quotes */
it('should find calls to require in code', () => {
const text = `
Component is described here.
@example ./extra.examples.md
@foo bar
`;
const expected = `
Component is described here.
`;
const actual = removeDoclets(text);
expect(actual).toBe(expected);
});
================================================
FILE: src/loaders/utils/__tests__/requireIt.spec.ts
================================================
import { generate } from 'escodegen';
import requireIt from '../requireIt';
it('requireIt() should return an AST for require statement', () => {
const result = requireIt('foo');
expect(result).toBeTruthy();
expect(typeof result.toAST).toBe('function');
expect(generate(result.toAST())).toBe("require('foo')");
});
================================================
FILE: src/loaders/utils/__tests__/resolveESModule.spec.ts
================================================
import { generate } from 'escodegen';
import { builders as b } from 'ast-types';
import resolveESModule from '../resolveESModule';
it('should return an array of AST', () => {
const result = resolveESModule('path/to/module', 'NameOfVar');
expect(generate(b.program(result))).toMatchInlineSnapshot(`
"const NameOfVar$0 = require('path/to/module');
const NameOfVar = NameOfVar$0.default || (NameOfVar$0['NameOfVar'] || NameOfVar$0);"
`);
});
================================================
FILE: src/loaders/utils/__tests__/sortProps.spec.ts
================================================
import { PropDescriptor, PropTypeDescriptor } from 'react-docgen';
import sortProps from '../sortProps';
function makeProp(
name: string,
required = false,
defaultValue: any = undefined,
type: PropTypeDescriptor = { name: 'string' }
): PropDescriptor {
return {
name,
required,
defaultValue,
type,
};
}
it('should sort required props', () => {
const props = [makeProp('prop2', true), makeProp('prop1', true)];
const result = sortProps(props);
expect(result.map(prop => prop.name)).toEqual(['prop1', 'prop2']);
});
it('should sort optional props', () => {
const props = [makeProp('prop2', false), makeProp('prop1', false)];
const result = sortProps(props);
expect(result.map(prop => prop.name)).toEqual(['prop1', 'prop2']);
});
it('should sort mixed props (required props should come first)', () => {
const props = [
makeProp('prop2', false),
makeProp('prop1', true),
makeProp('prop3', true),
makeProp('prop4', false),
];
const result = sortProps(props);
expect(result.map(prop => prop.name)).toEqual(['prop1', 'prop3', 'prop2', 'prop4']);
});
================================================
FILE: src/loaders/utils/chunkify.ts
================================================
import remark from 'remark';
import visit from 'unist-util-visit';
import highlightCode from './highlightCode';
import parseExample, { ExampleError } from './parseExample';
import * as Rsg from '../../typings';
const PLAYGROUND_LANGS = ['javascript', 'js', 'jsx', 'typescript', 'ts', 'tsx'];
const CODE_PLACEHOLDER = '<%{#code#}%>';
function isErrorExample(example: any): example is ExampleError {
return !!example.error;
}
/**
* Separate Markdown and code examples that should be rendered as a playground in a style guide.
*
* @param {string} markdown
* @param {Function} updateExample
* @param {Array} playgroundLangs
* @returns {Array}
*/
export default function chunkify(
markdown: string,
updateExample?: (example: Omit) => Omit,
playgroundLangs = PLAYGROUND_LANGS
): (Rsg.CodeExample | Rsg.MarkdownExample)[] {
const codeChunks: Rsg.CodeExample[] = [];
/*
* - Highlight code in fenced code blocks with defined language (```html).
* - Extract indented and fenced code blocks with lang javascript|js|jsx or if lang is not defined.
* - Leave all other Markdown or HTML as is.
*/
function processCode() {
return (ast: any) => {
visit(ast, 'code', (node: any) => {
const example = parseExample(node.value, node.lang, node.meta, updateExample);
if (isErrorExample(example)) {
node.lang = undefined;
node.value = example.error;
return;
}
const lang = example.lang;
node.lang = lang;
if (
!lang ||
(playgroundLangs.indexOf(lang) !== -1 && !(example.settings && example.settings.static))
) {
codeChunks.push({
type: 'code',
content: example.content,
settings: example.settings,
});
node.type = 'html';
node.value = CODE_PLACEHOLDER;
} else {
node.meta = null;
node.value = highlightCode(example.content, lang);
}
});
};
}
const rendered = remark().use(processCode).processSync(markdown).toString();
const chunks: (Rsg.CodeExample | Rsg.MarkdownExample)[] = [];
const textChunks = rendered.split(CODE_PLACEHOLDER);
textChunks.forEach((chunk) => {
chunk = chunk.trim();
if (chunk) {
chunks.push({
type: 'markdown',
content: chunk,
});
}
const code = codeChunks.shift();
if (code) {
chunks.push(code);
}
});
return chunks;
}
================================================
FILE: src/loaders/utils/client/__tests__/.eslintrc
================================================
{
"root": true,
"parser": "babel-eslint",
"extends": "tamia"
}
================================================
FILE: src/loaders/utils/client/__tests__/evalInContext.spec.js
================================================
import evalInContext from '../evalInContext';
describe('evalInContext', () => {
test('return a function', () => {
const result = evalInContext(`alert('header')`, (a) => a, `alert('code')`);
expect(typeof result).toBe('function');
});
test('create a separate scope for the body', () => {
const fn = () =>
evalInContext(
`const react = require('react')`,
(a) => a,
`const react = require('react')
const x = 42
`
);
expect(fn).not.toThrow();
});
});
================================================
FILE: src/loaders/utils/client/__tests__/requireInRuntime.spec.js
================================================
import requireInRuntime from '../requireInRuntime';
const map = {
a: () => 'a',
};
test('return a module from the map', () => {
const result = requireInRuntime(map, 'a');
expect(result).toBeDefined();
expect(result()).toBe('a');
});
test('throw if module is not in the map', () => {
const fn = () => requireInRuntime(map, 'pizza');
expect(fn).toThrowError('require() statements can be added');
});
================================================
FILE: src/loaders/utils/client/evalInContext.ts
================================================
/**
* Eval example code in a custom context:
* - `require()` that allows you to require modules from Markdown examples
* (won’t work dinamically becasue we need to know all required modules in
* advance to be able to bundle them with the code).
* - `state` variable, `setState` function that will be binded to a React
* component that manages example’s state on the frontend.
*
* Also prepends a given `code` with a `header` (maps required context modules
* to local variables: React, current component and modules defined via the
* `context` config option).
*/
export default function evalInContext(
header: string,
require: (module: string) => any,
code: string
): (state: Record, setState: any) => any {
// 1. Prepend code with the header
// 2. Wrap code in a block (`{}`) to create a new scope, so you could
// explicitly import context modules in your examples)
const body = `${header}
{${code}}`;
// eslint-disable-next-line no-new-func
const func = new Function('require', 'state', 'setState', body);
// Bind the `require` function, other context arguments will be passed from
// the frontend
return func.bind(null, require);
}
================================================
FILE: src/loaders/utils/client/requireInRuntime.ts
================================================
type Module = { [name: string]: any } | (() => any);
type RequireMap = { [filepath: string]: Module };
/**
* Return module from a given map (like {react: require('react')}) or throw.
* We allow to require modules only from Markdown examples (won’t work dynamically because we need to know all required
* modules in advance to be able to bundle them with the code).
*/
export default function requireInRuntime(requireMap: RequireMap, filepath: string): Module {
if (!(filepath in requireMap)) {
throw new Error(
`import or require() statements can be added only by editing a Markdown example file: ${filepath}`
);
}
return requireMap[filepath];
}
================================================
FILE: src/loaders/utils/expandDefaultComponent.ts
================================================
const COMPONENT_PLACEHOLDER = '__COMPONENT__';
const COMPONENT_PLACEHOLDER_REGEXP = new RegExp(COMPONENT_PLACEHOLDER, 'g');
/**
* Wrap a string with require() statement.
*
* @param {string} source Source code.
* @param {string} componentName Name that will be used instead of a placeholder.
* @returns {string}
*/
export default function expandDefaultComponent(source: string, componentName: string): string {
return source.replace(COMPONENT_PLACEHOLDER_REGEXP, componentName);
}
================================================
FILE: src/loaders/utils/filterComponentsWithExample.ts
================================================
import * as Rsg from '../../typings';
/**
* Filter out components without an example file.
*
* @param {Array} sections
* @returns {Array}
*/
export default function filterComponentsWithExample(
sections: Rsg.LoaderSection[]
): Rsg.LoaderSection[] {
return sections
.map(section => ({
...section,
sections: filterComponentsWithExample(section.sections),
components: section.components.filter(component => component.hasExamples),
}))
.filter(
section => section.components.length > 0 || section.sections.length > 0 || section.content
);
}
================================================
FILE: src/loaders/utils/getAllContentPages.ts
================================================
import * as Rsg from '../../typings';
/**
* Get all section content pages.
*
* @param {Array} sections
* @returns {Array}
*/
export default function getAllContentPages(
sections: Rsg.LoaderSection[]
): (Rsg.MarkdownExample | Rsg.RequireItResult)[] {
return sections.reduce((pages: (Rsg.MarkdownExample | Rsg.RequireItResult)[], section) => {
if (section.content) {
pages = pages.concat([section.content]);
}
if (section.sections) {
pages = pages.concat(getAllContentPages(section.sections));
}
return pages;
}, []);
}
================================================
FILE: src/loaders/utils/getAst.ts
================================================
import { Parser, Node as AcornNode, Options } from 'acorn';
import Logger from 'glogg';
const logger = Logger('rsg');
export const ACORN_OPTIONS: Options = {
ecmaVersion: 2019,
sourceType: 'module',
};
/**
* Parse source code with Acorn and return AST, returns undefined in case of errors
*/
export default function getAst(
code: string,
plugins: ((BaseParser: typeof Parser) => typeof Parser)[] = []
): AcornNode | undefined {
const parser = Parser.extend(...plugins);
try {
return parser.parse(code, ACORN_OPTIONS);
} catch (err) {
if (err instanceof Error) {
logger.debug(`Acorn cannot parse example code: ${err.message}\n\nCode:\n${code}`);
return undefined;
}
return undefined;
}
}
================================================
FILE: src/loaders/utils/getComponentFiles.ts
================================================
import glob from 'glob';
import path from 'path';
import isFunction from 'lodash/isFunction';
import isString from 'lodash/isString';
const getComponentGlobs = (components: string | string[] | (() => string[])): string[] => {
if (isFunction(components)) {
return components();
} else if (Array.isArray(components)) {
return components;
} else if (isString(components)) {
return [components];
}
throw new Error(
`Styleguidist: components should be string, function or array, received ${typeof components}.`
);
};
const getFilesMatchingGlobs = (components: string[], rootDir?: string, ignore?: string[]) => {
ignore = ignore || [];
return components
.map(listItem =>
glob.sync(listItem, {
cwd: rootDir,
ignore,
absolute: true,
})
)
.reduce((accumulator, current) => accumulator.concat(current), []);
};
/**
* Return absolute paths of components that should be rendered in the style guide.
*
* @param {string|Function|Array} components Function, Array or glob pattern.
* @param {string} rootDir
* @param {Array} [ignore] Glob patterns to ignore.
* @returns {Array}
*/
export default function getComponentFiles(
components?: string | string[] | (() => string[]) | undefined,
rootDir?: string,
ignore?: string[]
): string[] {
if (!components) {
return [];
}
// Normalize components option into an Array
const componentGlobs = getComponentGlobs(components);
// Resolve list of components from globs
const componentFiles = getFilesMatchingGlobs(componentGlobs, rootDir, ignore);
// Get absolute component file paths with correct slash separator format
const resolvedComponentFiles = componentFiles.map(file => path.resolve(file));
return resolvedComponentFiles;
}
================================================
FILE: src/loaders/utils/getComponentFilesFromSections.ts
================================================
import getComponentFiles from './getComponentFiles';
import * as Rsg from '../../typings';
/**
* Return absolute paths of all components in sections.
*
* @param {Array} sections
* @param {string} rootDir
* @param {Array} [ignore] Glob patterns to ignore.
* @returns {Array}
*/
export default function getComponentFilesFromSections(
sections: Rsg.ConfigSection[],
rootDir?: string,
ignore?: string[]
): string[] {
return sections.reduce((components: string[], section) => {
if (section.components) {
return components.concat(getComponentFiles(section.components, rootDir, ignore));
}
if (section.sections) {
return components.concat(getComponentFilesFromSections(section.sections, rootDir, ignore));
}
return components;
}, []);
}
================================================
FILE: src/loaders/utils/getComponentPatternsFromSections.ts
================================================
import * as Rsg from '../../typings';
/**
* Return all glob patterns from all sections.
*
* NOTE: a section cannot have components & subsections
* @param {Array} sections
* @returns {Array}
*/
export default function getComponentPatternsFromSections(sections: Rsg.ConfigSection[]): string[] {
return sections.reduce((patterns: string[], section) => {
if (Array.isArray(section.components)) {
return patterns.concat(section.components);
}
if (section.sections) {
return patterns.concat(getComponentPatternsFromSections(section.sections));
}
return patterns;
}, []);
}
================================================
FILE: src/loaders/utils/getComponents.ts
================================================
import processComponent from './processComponent';
import * as Rsg from '../../typings';
/**
* Process each component in a list.
*
* @param {Array} components File names of components.
* @param {object} config
* @returns {object|null}
*/
export default function getComponents(
components: string[],
config: Rsg.SanitizedStyleguidistConfig
) {
return components.map(filepath => processComponent(filepath, config));
}
================================================
FILE: src/loaders/utils/getExamples.ts
================================================
import path from 'path';
import fs from 'fs';
import { encode } from 'qss';
import requireIt from './requireIt';
import * as Rsg from '../../typings';
const examplesLoader = path.resolve(__dirname, '../examples-loader.js');
/**
* Get require statement for examples file if it exists, or for default examples if it was defined.
*/
export default function getExamples(
file: string,
displayName: string,
examplesFile?: string | false,
defaultExample?: string | false
): Rsg.RequireItResult | null {
const examplesFileToLoad =
(examplesFile && fs.existsSync(examplesFile) ? examplesFile : false) || defaultExample;
if (!examplesFileToLoad) {
return null;
}
const relativePath = `./${path.relative(path.dirname(examplesFileToLoad), file)}`;
const query = {
displayName,
file: relativePath,
shouldShowDefaultExample: !examplesFile && !!defaultExample,
};
return requireIt(`!!${examplesLoader}?${encode(query)}!${examplesFileToLoad}`);
}
================================================
FILE: src/loaders/utils/getImports.ts
================================================
import acornJsx from 'acorn-jsx';
import { walk } from 'estree-walker';
import getAst from './getAst';
/**
* Returns a list of all strings used in import statements or require() calls
*/
export default function getImports(code: string): string[] {
// Parse example source code, but ignore errors:
// 1. Adjacent JSX elements must be wrapped in an enclosing tag ( ) -
// imports/requires are not allowed in this case, and we'll wrap the code
// in React.Fragment on the frontend
// 2. All other errors - we'll deal with them on the frontend
const ast = getAst(code, [acornJsx()]);
if (!ast) {
return [];
}
const imports: string[] = [];
walk(ast as any, {
enter: (node: any) => {
// import foo from 'foo'
// import 'foo'
if (node.type === 'ImportDeclaration') {
if (node.source) {
imports.push(node.source.value);
}
}
// require('foo')
else if (node.type === 'CallExpression') {
if (
node.callee &&
node.callee.name === 'require' &&
node.arguments &&
node.arguments[0].value
) {
imports.push(node.arguments[0].value);
}
}
},
});
return imports;
}
================================================
FILE: src/loaders/utils/getNameFromFilePath.ts
================================================
import path from 'path';
import startCase from 'lodash/startCase';
/**
* your-buttonTS -> YourButtonTS
* your_button--TS -> YourButtonTS
*/
function transformFileNameToDisplayName(displayName: string): string {
return startCase(displayName).replace(/\s/g, '');
}
export default function getNameFromFilePath(filePath: string): string {
let fileName = path.basename(filePath, path.extname(filePath));
if (fileName === 'index') {
fileName = path.basename(path.dirname(filePath));
}
return transformFileNameToDisplayName(fileName);
}
================================================
FILE: src/loaders/utils/getProps.ts
================================================
import path from 'path';
import fs from 'fs';
import { TagProps, TagParamObject, DocumentationObject, utils, TagObject } from 'react-docgen';
import _ from 'lodash';
import doctrine, { Annotation } from 'doctrine';
import createLogger from 'glogg';
import highlightCodeInMarkdown from './highlightCodeInMarkdown';
import removeDoclets from './removeDoclets';
import requireIt from './requireIt';
import getNameFromFilePath from './getNameFromFilePath';
import * as Rsg from '../../typings';
const logger = createLogger('rsg');
const examplesLoader = path.resolve(__dirname, '../examples-loader.js');
const JS_DOC_METHOD_PARAM_TAG_SYNONYMS: (keyof TagProps)[] = ['param', 'arg', 'argument'];
const JS_DOC_METHOD_RETURN_TAG_SYNONYMS: (keyof TagProps)[] = ['return', 'returns'];
const JS_DOC_ALL_SYNONYMS: (keyof TagProps)[] = [
...JS_DOC_METHOD_PARAM_TAG_SYNONYMS,
...JS_DOC_METHOD_RETURN_TAG_SYNONYMS,
];
// HACK: We have to make sure that doclets is a proper object with correct prototype to
// work around an issue in react-docgen that breaks the build if a component has JSDoc tags
// like @see in its description, see https://github.com/reactjs/react-docgen/issues/155
// and https://github.com/styleguidist/react-styleguidist/issues/298
const getDocletsObject = (str?: string) => ({ ...utils.docblock.getDoclets(str) });
const getDoctrineTags = (documentation: Annotation) => {
return _.groupBy(documentation.tags, 'title');
};
const doesExternalExampleFileExist = (componentPath: string, exampleFile: string) => {
const exampleFilepath = path.resolve(path.dirname(componentPath), exampleFile);
const doesFileExist = fs.existsSync(exampleFilepath);
if (!doesFileExist) {
logger.warn(`An example file ${exampleFile} defined in ${componentPath} component not found.`);
}
return doesFileExist;
};
const getMergedTag = (tags: TagProps, names: (keyof TagProps)[]): TagObject[] => {
return names.reduce((params: TagObject[], name) => [...params, ...(tags[name] || [])], []);
};
/**
* 1. Remove non-public methods.
* 2. Extract doclets.
* 3. Highlight code in descriptions.
* 4. Extract @example doclet (load linked file with examples-loader).
*
* @param {object} doc
* @param {string} filepath
* @returns {object}
*/
export default function getProps(doc: DocumentationObject, filepath?: string): Rsg.TempPropsObject {
const outDocs: Rsg.TempPropsObject = { doclets: {}, displayName: '', ...doc, methods: undefined };
// Keep only public methods
outDocs.methods = (doc.methods || []).filter(method => {
const doclets = method.docblock && utils.docblock.getDoclets(method.docblock);
return doclets && doclets.public;
}) as Rsg.MethodWithDocblock[];
// Parse the docblock of the remaining methods with doctrine to retrieve
// the JSDoc tags
// if a method is visible it must have a docblock
outDocs.methods = outDocs.methods.map(method => {
const allTags = getDoctrineTags(
doctrine.parse(method.docblock, { sloppy: true, unwrap: true })
);
// Merge with react-docgen information about arguments and return value
// with information from JSDoc
const paramTags = getMergedTag(
allTags as TagProps,
JS_DOC_METHOD_PARAM_TAG_SYNONYMS
) as TagParamObject[];
const params =
method.params &&
method.params.map(param => ({
...param,
...paramTags.find(tagParam => tagParam.name === param.name),
}));
if (params) {
method.params = params;
}
const returnTags = getMergedTag(
allTags as TagProps,
JS_DOC_METHOD_RETURN_TAG_SYNONYMS
) as TagParamObject[];
const returns = method.returns
? {
...method.returns,
type: {
type: 'NameExpression',
...method.returns.type,
},
}
: returnTags[0];
if (returns) {
method.returns = returns;
}
// Remove tag synonyms
method.tags = _.omit(allTags, JS_DOC_ALL_SYNONYMS);
return method;
});
if (doc.description) {
// Read doclets from the description and remove them
outDocs.doclets = getDocletsObject(doc.description);
const documentation = doctrine.parse(doc.description);
outDocs.tags = getDoctrineTags(documentation) as TagProps;
outDocs.description = highlightCodeInMarkdown(removeDoclets(doc.description));
let exampleFileExists = false;
let exampleFile = outDocs.doclets.example;
// doc.doclets.example might be a boolean or undefined
if (typeof outDocs.doclets.example === 'string' && filepath) {
exampleFile = outDocs.doclets.example.trim();
exampleFileExists = doesExternalExampleFileExist(filepath, exampleFile);
}
if (exampleFileExists) {
outDocs.example = requireIt(`!!${examplesLoader}!${exampleFile}`);
delete outDocs.doclets.example;
}
} else {
outDocs.doclets = {};
}
if (doc.props) {
// Read doclets of props
Object.keys(doc.props).forEach(propName => {
if (!doc.props) {
return;
}
const prop = doc.props[propName];
const doclets = getDocletsObject(prop.description);
// When a prop is listed in defaultProps but not in props the prop.description is undefined
const documentation = doctrine.parse(prop.description || '');
// documentation.description is the description without tags
prop.description = documentation.description;
prop.tags = getDoctrineTags(documentation) as TagProps;
// Remove ignored props
if (doclets && doclets.ignore && outDocs.props) {
delete outDocs.props[propName];
} else if (outDocs.props) {
outDocs.props[propName] = prop;
}
});
}
if (!doc.displayName && filepath) {
// Guess the exported component's display name based on the file path
outDocs.displayName = getNameFromFilePath(filepath);
}
if (outDocs.doclets && outDocs.doclets.visibleName) {
outDocs.visibleName = outDocs.doclets.visibleName;
// Custom tag is added both to doclets and tags
// Removing from both locations
delete outDocs.doclets.visibleName;
if (outDocs.tags) {
delete outDocs.tags.visibleName;
}
}
return outDocs;
}
================================================
FILE: src/loaders/utils/getSections.ts
================================================
// This two functions should be in the same file because of cyclic imports
import fs from 'fs';
import path from 'path';
import _ from 'lodash';
import requireIt from './requireIt';
import getComponentFiles from './getComponentFiles';
import getComponents from './getComponents';
import slugger from './slugger';
import * as Rsg from '../../typings';
const examplesLoader = path.resolve(__dirname, '../examples-loader.js');
function processSectionContent(
section: Rsg.ConfigSection,
config: Rsg.SanitizedStyleguidistConfig
): Rsg.RequireItResult | Rsg.MarkdownExample | undefined {
if (!section.content) {
return undefined;
}
const contentRelativePath = section.content;
if (_.isFunction(section.content)) {
return {
type: 'markdown',
content: section.content(),
};
}
// Try to load section content file
const contentAbsolutePath = path.resolve(config.configDir, contentRelativePath);
if (!fs.existsSync(contentAbsolutePath)) {
throw new Error(`Styleguidist: Section content file not found: ${contentAbsolutePath}`);
}
return requireIt(`!!${examplesLoader}!${contentAbsolutePath}`);
}
const getSectionComponents = (
section: Rsg.ConfigSection,
config: Rsg.SanitizedStyleguidistConfig
) => {
let ignore = config.ignore ? _.castArray(config.ignore) : [];
if (section.ignore) {
ignore = ignore.concat(_.castArray(section.ignore));
}
return getComponents(getComponentFiles(section.components, config.configDir, ignore), config);
};
/**
* Return object for one level of sections.
*
* @param {Array} sections
* @param {object} config
* @param {number} parentDepth
* @returns {Array}
*/
export default function getSections(
sections: Rsg.ConfigSection[],
config: Rsg.SanitizedStyleguidistConfig,
parentDepth?: number
): Rsg.LoaderSection[] {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
return sections.map(section => processSection(section, config, parentDepth));
}
/**
* Return an object for a given section with all components and subsections.
* @param {object} section
* @param {object} config
* @param {number} parentDepth
* @returns {object}
*/
export function processSection(
section: Rsg.ConfigSection,
config: Rsg.SanitizedStyleguidistConfig,
parentDepth?: number
): Rsg.LoaderSection {
const content = processSectionContent(section, config);
let sectionDepth;
if (parentDepth === undefined) {
sectionDepth = section.sectionDepth !== undefined ? section.sectionDepth : 0;
} else {
sectionDepth = parentDepth === 0 ? 0 : parentDepth - 1;
}
return {
...section,
exampleMode: section.exampleMode || config.exampleMode,
usageMode: section.usageMode || config.usageMode,
sectionDepth,
slug: `section-${slugger.slug(section.name || 'untitled')}`,
sections: getSections(section.sections || [], config, sectionDepth),
href: section.href,
components: getSectionComponents(section, config),
content,
};
}
================================================
FILE: src/loaders/utils/highlightCode.ts
================================================
import createLogger from 'glogg';
import * as Prism from 'prismjs';
import 'prismjs/components/prism-clike';
import 'prismjs/components/prism-markup';
import 'prismjs/components/prism-markdown';
import 'prismjs/components/prism-css';
import 'prismjs/components/prism-css-extras';
import 'prismjs/components/prism-scss';
import 'prismjs/components/prism-less';
import 'prismjs/components/prism-javascript';
import 'prismjs/components/prism-flow';
import 'prismjs/components/prism-typescript';
import 'prismjs/components/prism-jsx';
import 'prismjs/components/prism-tsx';
import 'prismjs/components/prism-graphql';
import 'prismjs/components/prism-json';
import 'prismjs/components/prism-bash';
import 'prismjs/components/prism-diff';
const logger = createLogger('rsg');
const IGNORED_LANGUAGES = ['extend', 'insertBefore', 'DFS'];
const getLanguages = () => Object.keys(Prism.languages).filter(x => !IGNORED_LANGUAGES.includes(x));
/**
* Highlight code.
*
* @param {string} code
* @param {string} lang
* @returns {string}
*/
export default function highlightCode(code: string, lang?: string): string {
if (!lang) {
return code;
}
const grammar = Prism.languages[lang];
if (!grammar) {
logger.warn(
`Syntax highlighting for “${lang}” isn’t supported. Supported languages are: ${getLanguages().join(
', '
)}.`
);
return code;
}
return Prism.highlight(code, grammar, lang);
}
================================================
FILE: src/loaders/utils/highlightCodeInMarkdown.ts
================================================
import remark from 'remark';
import visit from 'unist-util-visit';
import highlightCode from './highlightCode';
function highlight() {
return (ast: any) => {
visit(ast, 'code', (node: any) => {
node.value = highlightCode(node.value, node.lang);
});
};
}
/**
* Highlight code in code snippets in Markdown.
*
* @param {string} markdown
* @returns {string}
*/
export default function highlightCodeInMarkdown(markdown: string): string {
return remark().use(highlight).processSync(markdown).toString();
}
================================================
FILE: src/loaders/utils/parseExample.ts
================================================
import lowercaseKeys from 'lowercase-keys';
import { DOCS_DOCUMENTING } from '../../scripts/consts';
import * as Rsg from '../../typings';
const hasStringModifiers = (modifiers: string): boolean => !!modifiers.match(/^[ \w]+$/);
export interface ExampleError {
error: string;
}
/**
* Split fenced code block header to lang and modifiers, parse modifiers, lowercase modifier keys, etc.
*/
export default function parseExample(
content: string,
lang?: string | null,
modifiers?: string,
updateExample: (example: Omit) => Omit = x => x
): Omit | ExampleError {
const example: Omit = {
content,
lang,
};
if (modifiers) {
if (hasStringModifiers(modifiers)) {
example.settings = modifiers.split(' ').reduce((obj: Record, modifier) => {
obj[modifier] = true;
return obj;
}, {});
} else {
try {
example.settings = JSON.parse(modifiers);
} catch (err) {
return {
error: `Cannot parse modifiers for "${modifiers}". Use space-separated strings or JSON:\n\n${DOCS_DOCUMENTING}`,
};
}
}
}
const updatedExample = updateExample(example);
return {
...updatedExample,
settings: lowercaseKeys(updatedExample.settings || {}),
};
}
================================================
FILE: src/loaders/utils/processComponent.ts
================================================
import fs from 'fs';
import path from 'path';
import getNameFromFilePath from './getNameFromFilePath';
import requireIt from './requireIt';
import slugger from './slugger';
import * as Rsg from '../../typings';
const propsLoader = path.resolve(__dirname, '../props-loader.js');
/**
* References the filepath of the metadata file.
*
* @param {string} filepath
* @returns {string}
*/
function getComponentMetadataPath(filepath: string): string {
const extname = path.extname(filepath);
return filepath.substring(0, filepath.length - extname.length) + '.json';
}
/**
* Return an object with all required for style guide information for a given component.
*
* @param {string} filepath
* @param {object} config
* @returns {object}
*/
export default function processComponent(
filepath: string,
config: Rsg.SanitizedStyleguidistConfig
): Rsg.LoaderComponent {
const componentPath = path.relative(config.configDir, filepath);
const componentName = getNameFromFilePath(filepath);
const examplesFile = config.getExampleFilename(filepath);
const componentMetadataPath = getComponentMetadataPath(filepath);
return {
filepath: componentPath,
slug: slugger.slug(componentName),
pathLine: config.getComponentPathLine(componentPath),
module: requireIt(filepath),
props: requireIt(`!!${propsLoader}!${filepath}`),
hasExamples: !!(examplesFile && fs.existsSync(examplesFile)),
metadata: fs.existsSync(componentMetadataPath) ? requireIt(componentMetadataPath) : {},
};
}
================================================
FILE: src/loaders/utils/removeDoclets.ts
================================================
// Doclet regexp from react-docgen
const DOCLET_REGEXP = /^@(\w+)(?:$|\s((?:[^](?!^@\w))*))/gim;
/**
* Remove all doclets (e.g. `@example Foo.js`) from text.
* @param {string} text
* @returns {string}
*/
export default function removeDoclets(text: string) {
return text.replace(DOCLET_REGEXP, '');
}
================================================
FILE: src/loaders/utils/requireIt.ts
================================================
import { builders as b, ASTNode } from 'ast-types';
import * as Rsg from '../../typings';
/**
* Return a require() statement AST.
*
* @param {string} filepath Module name.
* @returns {object}
*/
export default function requireIt(filepath: string): Rsg.RequireItResult {
const obj = { require: filepath };
Object.defineProperty(obj, 'toAST', {
enumerable: false,
value(): ASTNode {
return b.callExpression(b.identifier('require'), [b.literal(filepath)]);
},
});
return obj as Rsg.RequireItResult;
}
================================================
FILE: src/loaders/utils/resolveESModule.ts
================================================
import { builders as b } from 'ast-types';
import requireIt from './requireIt';
/**
* Resolve ES5 requires for export default, named export and module.exports
*
* @param requireRequest the argument of the `require` function
* @param name the name of the resulting variable
* @returns AST
*/
export default (requireRequest: string, name: string) => {
// The name could possibly contain invalid characters for a JS variable name
// such as "." or "-".
const safeName = name ? name.replace(/\W/, '') : name;
return [
// const safeName$0 = require(path);
b.variableDeclaration('const', [
b.variableDeclarator(b.identifier(`${safeName}$0`), requireIt(requireRequest).toAST() as any),
]),
// const safeName = safeName$0.default || safeName$0[safeName] || safeName$0;
b.variableDeclaration('const', [
b.variableDeclarator(
b.identifier(safeName),
b.logicalExpression(
'||',
b.identifier(`${safeName}$0.default`),
b.logicalExpression('||', b.identifier(`${safeName}$0['${safeName}']`), b.identifier(`${safeName}$0`))
)
),
]),
]
};
================================================
FILE: src/loaders/utils/slugger.ts
================================================
import GithubSlugger from 'github-slugger';
// Export the singleton instance of GithubSlugger
export default new GithubSlugger();
================================================
FILE: src/loaders/utils/sortProps.ts
================================================
import sortBy from 'lodash/sortBy';
import { PropDescriptor } from 'react-docgen';
/**
* Sorts an array of properties by their 'required' property first and 'name'
* property second.
*
* @param {array} props
* @return {array} Sorted properties
*/
function sortProps(props: PropDescriptor[]) {
const requiredPropNames = sortBy(props.filter(prop => prop.required), 'name');
const optionalPropNames = sortBy(props.filter(prop => !prop.required), 'name');
const sortedProps = requiredPropNames.concat(optionalPropNames);
return sortedProps;
}
export default sortProps;
================================================
FILE: src/scripts/__mocks__/build.ts
================================================
import * as Rsg from '../../typings';
export default function build(
config: Rsg.SanitizedStyleguidistConfig,
callback: (err: Error | null, stats: any) => void
) {
callback(null, { stats: true });
return {};
}
================================================
FILE: src/scripts/__mocks__/server.ts
================================================
import * as Rsg from '../../typings';
export default function server(
config: Rsg.SanitizedStyleguidistConfig,
callback: (err: Error | null) => void
) {
callback(null);
return {};
}
================================================
FILE: src/scripts/__tests__/__snapshots__/make-webpack-config.spec.ts.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should merge user webpack config 1`] = `
Object {
"foo": "bar",
"rsg-components": "~/src/client/rsg-components",
}
`;
exports[`should not owerwrite user DefinePlugin 1`] = `
Array [
DefinePlugin {
"definitions": Object {
"process.env.NODE_ENV": "\\"test\\"",
"process.env.STYLEGUIDIST_ENV": "\\"development\\"",
},
},
DefinePlugin {
"definitions": Object {
"process.env.PIZZA": "\\"salami\\"",
},
},
]
`;
exports[`should prepend requires as webpack entries 1`] = `
Array [
"a/b.js",
"c/d.css",
"~/src/client/index",
]
`;
exports[`should set aliases 1`] = `
Object {
"rsg-components": "~/src/client/rsg-components",
}
`;
exports[`should set aliases from moduleAliases option 1`] = `
Object {
"foo": "bar",
"rsg-components": "~/src/client/rsg-components",
}
`;
exports[`should set aliases from styleguideComponents option 1`] = `
Object {
"rsg-components": "~/src/client/rsg-components",
"rsg-components/foo": "bar",
}
`;
================================================
FILE: src/scripts/__tests__/config.spec.ts
================================================
import fs from 'fs';
import path from 'path';
import { Configuration } from 'webpack';
import getConfig from '../config';
const testComponent = (name: string) => path.resolve(__dirname, '../../../test/components', name);
const cwd = process.cwd();
const configDir = path.resolve(__dirname, '../../../test/apps/defaults');
beforeEach(() => {
process.chdir(configDir);
});
afterAll(() => {
process.chdir(cwd);
});
it('should read a config file', () => {
const result = getConfig('../basic/styleguide.config.js');
expect(result).toMatchObject({ title: 'React Style Guide Example' });
});
it('should accept absolute path', () => {
const result = getConfig(path.join(__dirname, '../../../test/apps/basic/styleguide.config.js'));
expect(result).toMatchObject({ title: 'React Style Guide Example' });
});
it('should throw when passed config file not found', () => {
const fn = () => getConfig('pizza');
expect(fn).toThrow();
});
it('should find config file automatically', () => {
process.chdir('../basic');
const result = getConfig();
expect(result).toMatchObject({ title: 'React Style Guide Example' });
});
it('should accept config as an object', () => {
const result = getConfig({
title: 'Style guide',
});
expect(result).toMatchObject({ title: 'Style guide' });
});
it('should throw if config has errors', () => {
expect.assertions(1);
try {
getConfig({
components: 42,
} as any);
} catch (err) {
if (err instanceof Error) {
expect(err.message).toMatch('should be string, function, or array');
}
}
});
it('should change the config using the update callback', () => {
const result = getConfig(
{
title: 'Style guide',
},
(config) => {
config.title = 'Pizza';
return config;
}
);
expect(result).toMatchObject({ title: 'Pizza' });
});
it('should have default getExampleFilename implementation', () => {
const result = getConfig();
expect(typeof result.getExampleFilename).toEqual('function');
});
it('default getExampleFilename should return Readme.md path if it exists', () => {
process.chdir('../..');
const result = getConfig();
expect(result.getExampleFilename(testComponent('Button/Button.js'))).toEqual(
testComponent('Button/Readme.md')
);
});
it('default getExampleFilename should return Component.md path if it exists', () => {
process.chdir('../..');
const result = getConfig();
expect(result.getExampleFilename(testComponent('Placeholder/Placeholder.js'))).toEqual(
testComponent('Placeholder/Placeholder.md')
);
});
it('default getExampleFilename should return Component.md path if it exists with index.js', () => {
process.chdir('../..');
const result = getConfig();
result.components = './components/**/*.js';
expect(result.getExampleFilename(testComponent('Label/Label.js'))).toEqual(
testComponent('Label/Label.md')
);
});
it('default getExampleFilename should return false if no examples file found', () => {
process.chdir('../..');
const result = getConfig();
expect(result.getExampleFilename(testComponent('RandomButton/RandomButton.js'))).toBeFalsy();
});
it('should have default getComponentPathLine implementation', () => {
const result = getConfig();
expect(typeof result.getComponentPathLine).toEqual('function');
expect(result.getComponentPathLine('components/Button.js')).toEqual('components/Button.js');
});
it('should have default title based on package.json name', () => {
const result = getConfig();
expect(result.title).toEqual('Pizza Style Guide');
});
it('configDir option should be a directory of a passed config', () => {
const result = getConfig(path.join(configDir, 'styleguide.config.js'));
expect(result).toMatchObject({ configDir });
});
it('configDir option should be a current directory if the config was passed as an object', () => {
const result = getConfig();
expect(result).toMatchObject({ configDir: process.cwd() });
});
it('should absolutize assetsDir if it exists', () => {
const assetsDir = 'src/components';
const result = getConfig({
assetsDir,
});
expect(result.assetsDir).toEqual(path.join(configDir, assetsDir));
});
it('should throw if assetsDir does not exist', () => {
const fn = () =>
getConfig({
assetsDir: 'pizza',
});
expect(fn).toThrow();
});
it('should use embedded default example template if defaultExample=true', (done) => {
const result = getConfig({
defaultExample: true,
});
expect(typeof result.defaultExample).toEqual('string');
if (typeof result.defaultExample === 'string') {
expect(fs.existsSync(result.defaultExample)).toBeTruthy();
} else {
done.fail();
}
done();
});
it('should absolutize defaultExample if it is a string', () => {
const result = getConfig({
defaultExample: 'src/components/Button.md',
});
expect(result.defaultExample).toMatch(/^\//);
});
it('should throw if defaultExample does not exist', () => {
expect.assertions(1);
try {
getConfig({
defaultExample: 'pizza',
});
} catch (err) {
if (err instanceof Error) {
expect(err.message).toMatch('does not exist');
}
}
});
it('should use components option as the first sections if there’s no sections option', () => {
const components = 'components/*/*.js';
const result = getConfig({
components,
});
expect(result.sections).toHaveLength(1);
expect(result.sections[0].components).toEqual(components);
});
it('should use default components option both components and sections options weren’t specified', () => {
const result = getConfig();
expect(result.sections).toHaveLength(1);
expect(result.sections[0].components).toMatch('**');
});
it('should ignore components option there’s sections options', () => {
const components = 'components/*/*.js';
const result = getConfig({
components: 'components/Button/*.js',
sections: [
{
components,
},
],
});
expect(result.sections).toHaveLength(1);
expect(result.sections[0].components).toEqual(components);
});
it('should return webpackConfig option as is', () => {
const webpackConfig = { mode: 'development' } as Configuration;
const result = getConfig({
webpackConfig,
});
expect(result.webpackConfig).toEqual(webpackConfig);
});
it('should return webpackConfig with user webpack config', () => {
process.chdir('../basic');
const result = getConfig();
expect(result.webpackConfig).toEqual(
expect.objectContaining({
module: {
rules: expect.any(Array),
},
})
);
});
it('should allow no webpack config', () => {
process.chdir('../no-webpack');
const fn = () => getConfig();
expect(fn).not.toThrow();
});
it('should throw when old template as a string option passed', () => {
expect.assertions(1);
try {
getConfig({
template: 'pizza',
});
} catch (err) {
if (err instanceof Error) {
expect(err.message).toMatch('format has been changed');
}
}
});
it('should throw when editorConfig option passed', () => {
expect.assertions(1);
try {
getConfig({
editorConfig: { theme: 'foo' },
});
} catch (err) {
if (err instanceof Error) {
expect(err.message).toMatch('config option was removed');
}
}
});
it('mountPointId should have default value', () => {
const result = getConfig();
expect(result.mountPointId).toEqual('rsg-root');
});
it('mountPointId should have default value', () => {
const result = getConfig();
expect(result.mountPointId).toEqual('rsg-root');
});
it('should set the exampleMode to expand if the flag showCode is on', () => {
const result = getConfig({
showCode: true,
});
expect(result.exampleMode).toBe('expand');
});
it('should set the exampleMode to collapse if the flag showCode is off', () => {
const result = getConfig({
showCode: false,
});
expect(result.exampleMode).toBe('collapse');
});
it('should set the usageMode to expand if the flag showUsage is on', () => {
const result = getConfig({
showUsage: true,
});
expect(result.usageMode).toBe('expand');
});
it('should set the usageMode to collapse if the flag showUsage is off', () => {
const result = getConfig({
showUsage: false,
});
expect(result.usageMode).toBe('collapse');
});
================================================
FILE: src/scripts/__tests__/create-server.spec.ts
================================================
import { WebpackOptionsNormalized } from 'webpack';
import createServer from '../create-server';
import getConfig from '../config';
const cwd = process.cwd();
afterEach(() => {
process.chdir(cwd);
});
test('createServer should return an object containing a server instance', () => {
process.chdir('test/apps/basic');
const config = getConfig();
const result = createServer(config, 'production');
expect(result).toBeTruthy();
expect(result.app).toBeTruthy();
});
test('createServer should support an array-valued assetsDir', (done) => {
process.chdir('test/apps/basic');
const config = getConfig({
assetsDir: ['src/components', 'src/components2'],
});
const result = createServer(config, 'production');
expect(result).toBeTruthy();
expect(result.app).toBeTruthy();
done();
});
test('createServer should return an object containing a production Webpack compiler', (done) => {
process.chdir('test/apps/basic');
const config = getConfig();
const result = createServer(config, 'production');
expect(result).toBeTruthy();
expect(result.compiler).toBeTruthy();
let output: WebpackOptionsNormalized['output'];
if (result.compiler && result.compiler.options && result.compiler.options.output) {
output = result.compiler.options.output;
} else {
done.fail('no output');
return;
}
expect(output.filename).toBe('build/bundle.[chunkhash:8].js');
expect(output.chunkFilename).toBe('build/[name].[chunkhash:8].js');
done();
});
test('createServer should return an object containing a development Webpack compiler', (done) => {
process.chdir('test/apps/basic');
const config = getConfig();
const result = createServer(config, 'development');
expect(result).toBeTruthy();
expect(result.compiler).toBeTruthy();
let output: WebpackOptionsNormalized['output'];
if (result.compiler && result.compiler.options && result.compiler.options.output) {
output = result.compiler.options.output;
} else {
done.fail('no output');
return;
}
expect(output.filename).toBe('build/[name].bundle.js');
expect(output.chunkFilename).toBe('build/[name].js');
done();
});
test('createServer should apply some base config options', () => {
process.chdir('test/apps/basic');
const config = {
...getConfig(),
serverHost: 'localhost',
serverPort: 6000,
};
const result = createServer(config, 'development');
expect(result).toBeTruthy();
expect(result.compiler).toBeTruthy();
expect(result.compiler.options.devServer).toMatchObject({
host: 'localhost',
port: 6000,
compress: true,
hot: true,
client: { logging: 'none' },
webSocketServer: 'ws',
});
});
test('createServer should allow overriding default devServer options', () => {
process.chdir('test/apps/basic');
const config = {
...getConfig(),
webpackConfig: {
devServer: {
client: {
overlay: false,
progress: true,
},
},
},
};
const result = createServer(config, 'development');
expect(result).toBeTruthy();
expect(result.compiler).toBeTruthy();
expect(result.compiler.options.devServer).toMatchObject({
client: {
overlay: false,
progress: true,
},
});
});
================================================
FILE: src/scripts/__tests__/index.esm.spec.ts
================================================
/* eslint-disable @typescript-eslint/no-var-requires */
import { Configuration } from 'webpack';
import last from 'lodash/last';
import styleguidist from '../index.esm';
jest.mock('../build');
jest.mock('../server');
const getDefaultWebpackConfig = () => styleguidist().makeWebpackConfig();
const cwd = process.cwd();
afterEach(() => {
process.chdir(cwd);
});
it('should return API methods', () => {
const api = styleguidist(require('../../../test/data/styleguide.config.js'));
expect(api).toBeTruthy();
expect(typeof api.build).toBe('function');
expect(typeof api.server).toBe('function');
expect(typeof api.makeWebpackConfig).toBe('function');
});
describe('makeWebpackConfig', () => {
it('should return development Webpack config', () => {
const api = styleguidist();
const result = api.makeWebpackConfig('development');
expect(result).toBeTruthy();
expect(result.output && result.output.filename).toBe('build/[name].bundle.js');
expect(result.output && result.output.chunkFilename).toBe('build/[name].js');
});
it('should return production Webpack config', () => {
const api = styleguidist();
const result = api.makeWebpackConfig('production');
expect(result).toBeTruthy();
expect(result.output && result.output.filename).toBe('build/bundle.[chunkhash:8].js');
expect(result.output && result.output.chunkFilename).toBe('build/[name].[chunkhash:8].js');
});
it('should merge webpackConfig config option', () => {
const defaultWebpackConfig = getDefaultWebpackConfig();
const api = styleguidist({
webpackConfig: {
resolve: {
extensions: ['.scss'],
},
},
});
const result = api.makeWebpackConfig();
expect(result).toBeTruthy();
expect(result?.resolve?.extensions).toHaveLength(
(defaultWebpackConfig?.resolve?.extensions?.length || 0) + 1
);
expect(last(result?.resolve?.extensions)).toEqual('.scss');
});
it('should merge webpackConfig but ignore output section', () => {
const defaultWebpackConfig = getDefaultWebpackConfig();
const api = styleguidist({
webpackConfig: {
resolve: {
extensions: ['.scss'],
},
output: {
filename: 'broken.js',
},
},
});
const result = api.makeWebpackConfig();
expect(result.output && result.output.filename).toEqual(
defaultWebpackConfig.output && defaultWebpackConfig.output.filename
);
});
it('should merge webpackConfig config option as a function', () => {
const api = styleguidist({
webpackConfig: (env) =>
({
mode: env,
} as Configuration),
});
const result = api.makeWebpackConfig();
expect(result).toBeTruthy();
expect(result.mode).toEqual('production');
});
it('should apply updateWebpackConfig config option', () => {
const defaultWebpackConfig = getDefaultWebpackConfig();
const api = styleguidist({
dangerouslyUpdateWebpackConfig: (webpackConfig, env) => {
if (webpackConfig.resolve && webpackConfig.resolve.extensions) {
webpackConfig.resolve.extensions.push(env);
}
return webpackConfig;
},
});
const result = api.makeWebpackConfig();
expect(result).toBeTruthy();
expect(result?.resolve?.extensions).toHaveLength(
(defaultWebpackConfig?.resolve?.extensions?.length || 0) + 1
);
expect(last(result?.resolve?.extensions)).toEqual('production');
});
it('should merge Create React App Webpack config', () => {
process.chdir('test/apps/basic');
const api = styleguidist();
const result = api.makeWebpackConfig();
expect(result).toBeTruthy();
expect(result.module).toBeTruthy();
});
it('should add webpack entry for each require config option item', () => {
const modules = ['babel-polyfill', 'path/to/styles.css'];
const api = styleguidist({
require: modules,
});
const result = api.makeWebpackConfig();
expect(result.entry).toEqual(expect.arrayContaining(modules));
});
it('should add webpack alias for each styleguideComponents config option item', () => {
const api = styleguidist({
styleguideComponents: {
Wrapper: 'styleguide/components/Wrapper',
StyleGuideRenderer: 'styleguide/components/StyleGuide',
},
});
const result = api.makeWebpackConfig();
expect(result?.resolve?.alias).toMatchObject({
'rsg-components/Wrapper': 'styleguide/components/Wrapper',
'rsg-components/StyleGuide/StyleGuideRenderer': 'styleguide/components/StyleGuide',
});
});
});
describe('build', () => {
it('should pass style guide config and stats to callback', () => {
const config = {
components: '*.js',
};
const callback = jest.fn();
const api = styleguidist(config);
api.build(callback);
expect(callback).toBeCalled();
expect(callback.mock.calls[0][1].components).toBe(config.components);
expect(callback.mock.calls[0][2]).toEqual({ stats: true });
});
});
describe('server', () => {
it('should pass style guide config to callback', () => {
const config = {
components: '*.js',
};
const callback = jest.fn();
const api = styleguidist(config);
api.server(callback);
expect(callback).toBeCalled();
expect(callback.mock.calls[0][1].components).toBe(config.components);
});
});
================================================
FILE: src/scripts/__tests__/logger.spec.ts
================================================
import glogg from 'glogg';
import setupLogger from '../logger';
const logger = glogg('rsg');
afterEach(() => {
logger.removeAllListeners();
});
test('should setup custom logger function', () => {
const info = jest.fn();
const message = 'pizza';
setupLogger({ info }, false);
logger.info(message);
expect(info).toBeCalledWith(message);
});
test('should setup debug logger in verbose mode', () => {
const debug = jest.fn();
const message = 'pizza';
setupLogger({ debug }, true);
logger.debug(message);
expect(debug).toBeCalledWith(message);
});
test('should not setup debug logger in non-verbose mode', () => {
const debug = jest.fn();
const message = 'pizza';
setupLogger({ debug }, false);
logger.debug(message);
expect(debug).toHaveBeenCalledTimes(0);
});
test('should accept default loggers', () => {
const info = jest.fn();
const message = 'pizza';
setupLogger(undefined, false, { info });
logger.info(message);
expect(info).toBeCalledWith(message);
});
================================================
FILE: src/scripts/__tests__/make-webpack-config.spec.ts
================================================
import webpack, {
Compiler,
Configuration,
validate,
ValidationError,
WebpackPluginInstance,
} from 'webpack';
import CopyWebpackPlugin from 'copy-webpack-plugin';
import makeWebpackConfig from '../make-webpack-config';
import * as Rsg from '../../typings';
jest.mock('copy-webpack-plugin');
type WebpackPlugin = WebpackPluginInstance | ((this: Compiler, compiler: Compiler) => void) | '...';
const styleguideConfig = ({
styleguideDir: __dirname,
require: [],
title: 'Style Guide',
} as unknown) as Rsg.SanitizedStyleguidistConfig;
const getClasses = (plugins: WebpackPlugin[] = [], name: string): WebpackPlugin[] =>
plugins.filter((x) => x.constructor.name === name);
const getClassNames = (plugins: WebpackPlugin[] = []): string[] =>
plugins.map((x) => x.constructor.name);
const process$env$nodeEnv = process.env.NODE_ENV;
beforeEach(() => {
((CopyWebpackPlugin as unknown) as jest.Mock).mockClear();
});
afterEach(() => {
process.env.NODE_ENV = process$env$nodeEnv;
});
it('should return a development config', () => {
const env = 'development';
const config = makeWebpackConfig(styleguideConfig, env);
expect(() => validate(config)).not.toThrow(ValidationError);
expect(config).toMatchObject({
mode: env,
});
expect(config).not.toHaveProperty('optimization');
});
it('should return a production config', () => {
const env = 'production';
const config = makeWebpackConfig(styleguideConfig, env);
expect(() => validate(config)).not.toThrow(ValidationError);
const plugins = getClassNames(config.plugins);
expect(plugins).toContain('CleanWebpackPlugin');
expect(plugins).not.toContain('HotModuleReplacementPlugin');
expect(config).toMatchObject({
output: {
filename: expect.stringContaining('[chunkhash'),
chunkFilename: expect.stringContaining('[chunkhash'),
},
});
expect(config).toMatchObject({
mode: env,
});
const result = getClasses(config.optimization && config.optimization.minimizer, 'TerserPlugin');
expect(result).toHaveLength(1);
});
it('should set aliases', () => {
const result = makeWebpackConfig(styleguideConfig, 'development');
expect(result.resolve && result.resolve.alias).toMatchSnapshot();
});
it('should set aliases from moduleAliases option', () => {
const result = makeWebpackConfig(
{
...styleguideConfig,
moduleAliases: {
foo: 'bar',
},
},
'development'
);
expect(result.resolve && result.resolve.alias).toMatchSnapshot();
});
it('should set aliases from styleguideComponents option', () => {
const result = makeWebpackConfig(
{
...styleguideConfig,
styleguideComponents: {
foo: 'bar',
},
},
'development'
);
expect(result.resolve && result.resolve.alias).toMatchSnapshot();
});
it('should prepend requires as webpack entries', () => {
const result = makeWebpackConfig(
{ ...styleguideConfig, require: ['a/b.js', 'c/d.css'] },
'development'
);
expect(result.entry).toMatchSnapshot();
});
it('should enable verbose mode in CleanWebpackPlugin', () => {
const result = makeWebpackConfig({ ...styleguideConfig, verbose: true }, 'production');
expect((getClasses(result.plugins, 'CleanWebpackPlugin')[0] as any).verbose).toBe(true);
});
it('should set from with assetsDir in CopyWebpackPlugin', () => {
makeWebpackConfig({ ...styleguideConfig, assetsDir: '/assets/' }, 'production');
expect(CopyWebpackPlugin).toHaveBeenCalledWith({
patterns: [{ from: '/assets/' }],
});
});
it('should set array of from with assetsDir array in CopyWebpackPlugin', () => {
makeWebpackConfig({ ...styleguideConfig, assetsDir: ['/assets1/', '/assets2/'] }, 'production');
expect(CopyWebpackPlugin).toHaveBeenCalledWith({
patterns: [{ from: '/assets1/' }, { from: '/assets2/' }],
});
});
it('should merge user webpack config', () => {
const result = makeWebpackConfig(
{ ...styleguideConfig, webpackConfig: { resolve: { alias: { foo: 'bar' } } } },
'development'
);
expect(result.resolve && result.resolve.alias).toMatchSnapshot();
});
it('should not owerwrite user DefinePlugin', () => {
const result = makeWebpackConfig(
{
...styleguideConfig,
webpackConfig: {
plugins: [
new webpack.DefinePlugin({
'process.env.PIZZA': JSON.stringify('salami'),
}),
],
},
},
'development'
);
// Doesn’t really test that values won’t be overwritten, just that
// DefinePlugin is applied twice. To write a real test we’d have to run
// webpack
expect(getClasses(result.plugins, 'DefinePlugin')).toMatchSnapshot();
});
it('should update webpack config', () => {
const extensions = ['.web.js', '.js'];
const result = makeWebpackConfig(
{
...styleguideConfig,
dangerouslyUpdateWebpackConfig: (c: Configuration) => {
if (c.resolve) {
c.resolve.extensions = extensions;
}
return c;
},
},
'development'
);
expect(result.resolve && result.resolve.extensions).toEqual(extensions);
});
it('should pass template context to HTML plugin', () => {
const template = {
pizza: 'salami',
};
const result = makeWebpackConfig(
{
...styleguideConfig,
template,
},
'development'
);
expect(getClasses(result.plugins, 'MiniHtmlWebpackPlugin')[0]).toMatchObject({
options: {
context: template,
template: expect.any(Function),
},
});
});
it('should pass template function to HTML plugin', () => {
const template = () => ' ';
const result = makeWebpackConfig(
{
...styleguideConfig,
template,
},
'development'
);
expect(getClasses(result.plugins, 'MiniHtmlWebpackPlugin')[0]).toMatchObject({
options: {
context: expect.any(Object),
template,
},
});
});
it('should update NODE_ENV', () => {
process.env.NODE_ENV = '';
makeWebpackConfig(styleguideConfig, 'production');
expect(process.env.NODE_ENV).toBe('production');
});
it('should not overwrite NODE_ENV', () => {
makeWebpackConfig(styleguideConfig, 'production');
expect(process.env.NODE_ENV).toBe(process$env$nodeEnv);
});
it('should pass specified mountPointId to HTML plugin', () => {
const result = makeWebpackConfig(
{
...styleguideConfig,
mountPointId: 'foo-bar',
},
'development'
);
expect(
(getClasses(result.plugins, 'MiniHtmlWebpackPlugin')[0] as any).options.context.container
).toEqual('foo-bar');
});
================================================
FILE: src/scripts/__tests__/server.spec.ts
================================================
import server from '../server';
import getConfig from '../config';
jest.mock('../create-server', () => () => {
return {
app: {
startCallback: (cb: () => void) => cb(),
close: (cb: () => void) => cb(),
},
compiler: {},
};
});
test('server should return an object containing a server instance', () => {
const config = getConfig();
const callback = jest.fn();
const serverInfo = server(config, callback);
expect(callback).toBeCalled();
expect(serverInfo.app).toBeTruthy();
expect(serverInfo.compiler).toBeTruthy();
expect(typeof serverInfo.app.startCallback).toBe('function');
expect(typeof serverInfo.app.close).toBe('function');
});
================================================
FILE: src/scripts/build.ts
================================================
import webpack from 'webpack';
import makeWebpackConfig from './make-webpack-config';
import * as Rsg from '../typings';
export default function build(
config: Rsg.SanitizedStyleguidistConfig,
callback: (err: Error, stats: webpack.Stats) => void
) {
return webpack(makeWebpackConfig(config, 'production'), (err, stats) => {
// require('fs').writeFileSync('stats.json', JSON.stringify(stats.toJson()));
callback(err as Error, stats as webpack.Stats);
});
}
================================================
FILE: src/scripts/config.ts
================================================
import fs from 'fs';
import path from 'path';
import findup from 'findup';
import isString from 'lodash/isString';
import isPlainObject from 'lodash/isPlainObject';
import schema from './schemas/config';
import StyleguidistError from './utils/error';
import sanitizeConfig from './utils/sanitizeConfig';
import * as Rsg from '../typings';
const CONFIG_FILENAME = 'styleguide.config.js';
/**
* Try to find config file up the file tree.
*
* @return {string|boolean} Config absolute file path.
*/
function findConfigFile(): string | false {
let configDir;
try {
configDir = findup.sync(process.cwd(), CONFIG_FILENAME);
} catch (exception) {
return false;
}
return path.join(configDir, CONFIG_FILENAME);
}
/**
* Read, parse and validate config file or passed config.
*
* @param {object|string} [config] All config options or config file name or nothing.
* @param {function} [update] Change config object before running validation on it.
* @returns {object}
*/
function getConfig(
config?: string | Rsg.StyleguidistConfig,
update?: (conf: Rsg.StyleguidistConfig) => Rsg.StyleguidistConfig
): Rsg.SanitizedStyleguidistConfig {
let configFilepath: string | false = false;
if (isString(config)) {
// Load config from a given file
configFilepath = path.resolve(process.cwd(), config);
if (!fs.existsSync(configFilepath)) {
throw new StyleguidistError('Styleguidist config not found: ' + configFilepath + '.');
}
config = {};
} else if (!isPlainObject(config)) {
// Try to read config options from a file
configFilepath = findConfigFile();
config = {};
}
if (configFilepath) {
config = require(configFilepath);
}
if (!config || isString(config)) {
return {} as any;
}
if (update) {
config = update(config);
}
const configDir = configFilepath ? path.dirname(configFilepath) : process.cwd();
try {
return sanitizeConfig(config, schema, configDir) as any;
} catch (exception) {
if (exception instanceof StyleguidistError) {
throw new StyleguidistError(
`Something is wrong with your style guide config\n\n${exception.message}`,
exception.extra
);
} else {
throw exception;
}
}
}
export default getConfig;
================================================
FILE: src/scripts/consts.ts
================================================
export const HOMEPAGE = 'https://react-styleguidist.js.org/';
export const BUGS = 'https://github.com/styleguidist/react-styleguidist/issues';
export const DOCS_CONFIG = 'https://react-styleguidist.js.org/docs/configuration';
export const DOCS_COMPONENTS = 'https://react-styleguidist.js.org/docs/components';
export const DOCS_WEBPACK = 'https://react-styleguidist.js.org/docs/webpack';
export const DOCS_DOCUMENTING = 'https://react-styleguidist.js.org/docs/documenting';
export const DOCS_THIRDPARTIES = 'https://react-styleguidist.js.org/docs/thirdparties';
================================================
FILE: src/scripts/create-server.ts
================================================
import webpack from 'webpack';
import WebpackDevServer, { Configuration } from 'webpack-dev-server';
import makeWebpackConfig from './make-webpack-config';
import * as Rsg from '../typings';
export default function createServer(
config: Rsg.SanitizedStyleguidistConfig,
env: 'development' | 'production' | 'none'
): { app: WebpackDevServer; compiler: webpack.Compiler } {
const webpackConfig = makeWebpackConfig(config, env);
const baseConfig: Partial = {
host: config.serverHost,
port: config.serverPort,
compress: true,
hot: true,
client: {
logging: 'none',
},
static: Array.isArray(config.assetsDir)
? config.assetsDir.map((assetsDir) => ({
directory: assetsDir,
watch: true,
publicPath: '/',
}))
: {
directory: config.assetsDir,
watch: true,
publicPath: '/',
},
devMiddleware: {
stats: webpackConfig.stats || {},
},
};
// Allow custom devServer options to override base config.
webpackConfig.devServer = {
...baseConfig,
...webpackConfig.devServer,
};
const compiler = webpack(webpackConfig);
const devServer = new WebpackDevServer(webpackConfig.devServer, compiler);
// User defined customizations
if (config.configureServer) {
config.configureServer((devServer as any).app, env);
}
return { app: devServer, compiler };
}
================================================
FILE: src/scripts/index.esm.ts
================================================
import webpack from 'webpack';
// Make sure user has webpack installed
import './utils/ensureWebpack';
import makeWebpackConfig from './make-webpack-config';
import build from './build';
import server from './server';
import getConfig from './config';
import setupLogger from './logger';
import * as Rsg from '../typings';
/**
* Initialize Styleguide API.
*
* @param {object} [config] Styleguidist config.
* @returns {object} API.
*/
export default function (configArg?: Rsg.StyleguidistConfig | string) {
const config = getConfig(configArg, (conf) => {
setupLogger(conf.logger as Record void>, conf.verbose, {});
return conf;
});
return {
/**
* Build style guide.
*
* @param {Function} callback callback(err, config, stats).
* @return {Compiler} Webpack Compiler instance.
*/
build(
callback: (
err: Error,
styleguidistConfig: Rsg.SanitizedStyleguidistConfig,
stats: webpack.Stats
) => void
) {
return build(config, (err: Error, stats: webpack.Stats) => callback(err, config, stats));
},
/**
* Start style guide dev server.
*
* @param {Function} callback callback(err, config).
* @return {ServerInfo.App} Webpack-Dev-Server.
* @return {ServerInfo.Compiler} Webpack Compiler instance.
*/
server(
callback: (
err: Error | undefined,
styleguidistConfig: Rsg.SanitizedStyleguidistConfig
) => void
) {
return server(config, (err) => callback(err, config));
},
/**
* Return Styleguidist Webpack config.
*
* @param {string} [env=production] 'production' or 'development'.
* @return {object}
*/
makeWebpackConfig(env?: 'development' | 'production' | 'none') {
return makeWebpackConfig(config, env || 'production');
},
};
}
================================================
FILE: src/scripts/index.ts
================================================
// eslint-disable-next-line @typescript-eslint/no-var-requires
module.exports = require('./index.esm').default;
export * from '../typings';
================================================
FILE: src/scripts/logger.ts
================================================
/* eslint-disable no-console */
import _ from 'lodash/fp';
import kleur from 'kleur';
import loggerMaker from 'glogg';
const logger = loggerMaker('rsg');
const format = (message: string) => message.trim() + '\n';
const printers: Record void> = {
info: (message: string) => console.log(format(message)),
warn: (message: string) => console.warn(kleur.yellow(`Warning: ${format(message)}`)),
debug: (message: string) => console.log(format(message)),
};
/**
* Setup up logger:
* const logger = require('glogg')('rsg')
* logger.info('Drinking coffee...')
*
* @param {Object} methods Custom methods
* @param {bool} verbose Print debug messages
* @param {Object} [defaults] Default methods
*/
export default function setupLogger(
methods?: Record void>,
verbose?: boolean,
defaults?: Record void>
) {
_.flow(
_.defaults(defaults || printers),
_.omit(verbose ? [] : ['debug']),
_.toPairs,
_.forEach((printer: any[]) => logger.on(printer[0], printer[1]))
)(methods);
}
================================================
FILE: src/scripts/make-webpack-config.ts
================================================
import path from 'path';
import castArray from 'lodash/castArray';
import webpack, { Configuration, Resolver } from 'webpack';
import TerserPlugin from 'terser-webpack-plugin';
import { MiniHtmlWebpackPlugin } from 'mini-html-webpack-plugin';
import MiniHtmlWebpackTemplate from '@vxna/mini-html-webpack-template';
import { CleanWebpackPlugin } from 'clean-webpack-plugin';
import CopyWebpackPlugin from 'copy-webpack-plugin';
import merge from 'webpack-merge';
import forEach from 'lodash/forEach';
import isFunction from 'lodash/isFunction';
import StyleguidistOptionsPlugin from './utils/StyleguidistOptionsPlugin';
import mergeWebpackConfig from './utils/mergeWebpackConfig';
import * as Rsg from '../typings';
const RENDERER_REGEXP = /Renderer$/;
const sourceDir = path.resolve(__dirname, '../client');
interface AliasedConfiguration extends Configuration {
resolve: Resolver['resolve'] & { alias: Record };
}
export default function (
config: Rsg.SanitizedStyleguidistConfig,
env: 'development' | 'production' | 'none'
): Configuration {
process.env.NODE_ENV = process.env.NODE_ENV || env;
const isProd = env === 'production';
const template = isFunction(config.template) ? config.template : MiniHtmlWebpackTemplate;
const templateContext = isFunction(config.template) ? {} : config.template;
const htmlPluginOptions = {
context: {
lang: 'en',
...templateContext,
title: config.title,
container: config.mountPointId,
},
template,
};
let webpackConfig: Configuration = {
entry: config.require.concat([path.resolve(sourceDir, 'index')]),
mode: env,
output: {
path: config.styleguideDir,
filename: 'build/[name].bundle.js',
chunkFilename: 'build/[name].js',
publicPath: '',
},
resolve: {
extensions: ['.wasm', '.mjs', '.js', '.jsx', '.ts', '.tsx', '.json'],
alias: {},
},
plugins: [
new StyleguidistOptionsPlugin(config),
new MiniHtmlWebpackPlugin(htmlPluginOptions),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
'process.env.STYLEGUIDIST_ENV': JSON.stringify(env),
}),
],
performance: {
hints: false,
},
};
if (isProd) {
const minimizer = new TerserPlugin({
terserOptions: {
ie8: false,
ecma: 5,
compress: {
keep_fnames: true,
warnings: false,
/*
* Disable reduce_funcs to keep Terser from inlining
* Preact's VNode. If enabled, the 'new VNode()' is replaced
* with a anonymous 'function(){}', which is problematic for
* preact-compat, since it extends the VNode prototype to
* accomodate React's API.
*/
reduce_funcs: false,
},
mangle: {
keep_fnames: true,
},
},
});
webpackConfig = merge(webpackConfig, {
output: {
filename: 'build/bundle.[chunkhash:8].js',
chunkFilename: 'build/[name].[chunkhash:8].js',
},
plugins: [
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: [`${config.styleguideDir}/build/**/*`],
verbose: config.verbose === true,
}),
],
optimization: {
minimize: config.minimize === true,
minimizer: [minimizer],
},
});
if (config.assetsDir && webpackConfig.plugins) {
const copyPatterns = {
patterns: castArray(config.assetsDir).map((dir) => ({ from: dir })),
};
webpackConfig.plugins.push(
// FIXME: Since we don't have the type of copy-webpack-plugin@6.0
// we cast the config as any to make it work. Once the new types are
// released we must remove the cast.
new CopyWebpackPlugin(copyPatterns as any)
);
}
} else {
webpackConfig = merge(webpackConfig, {
devServer: {
webSocketServer: 'ws',
},
});
}
if (config.webpackConfig) {
webpackConfig = mergeWebpackConfig(webpackConfig, config.webpackConfig, env);
}
// Custom aliases
// NOTE: in a sanitized config, moduleAliases are always an object (never null or undefined)
const aliasedWebpackConfig = merge(webpackConfig, {
resolve: { alias: config.moduleAliases },
}) as AliasedConfiguration;
const alias = aliasedWebpackConfig.resolve.alias;
// Custom style guide components
if (config.styleguideComponents) {
forEach(config.styleguideComponents, (filepath, name) => {
const fullName = name.match(RENDERER_REGEXP)
? `${name.replace(RENDERER_REGEXP, '')}/${name}`
: name;
alias[`rsg-components/${fullName}`] = filepath;
});
}
// Add components folder alias at the end, so users can override our components
// to customize the style guide (their aliases should be before this one)
alias['rsg-components'] = path.resolve(sourceDir, 'rsg-components');
webpackConfig = config.dangerouslyUpdateWebpackConfig
? config.dangerouslyUpdateWebpackConfig(aliasedWebpackConfig, env)
: aliasedWebpackConfig;
return webpackConfig;
}
================================================
FILE: src/scripts/schemas/config.ts
================================================
// If you want to access any of these options in React, don’t forget to update CLIENT_CONFIG_OPTIONS array
// in loaders/styleguide-loader.js
import glogg from 'glogg';
import path from 'path';
import startCase from 'lodash/startCase';
import kleur from 'kleur';
import * as reactDocgen from 'react-docgen';
import { TransformOptions } from 'buble';
import { createDisplayNameHandler } from 'react-docgen-displayname-handler';
import annotationResolver from 'react-docgen-annotation-resolver';
import { ASTNode } from 'ast-types';
import { NodePath } from 'ast-types/lib/node-path';
import findUserWebpackConfig from '../utils/findUserWebpackConfig';
import getUserPackageJson from '../utils/getUserPackageJson';
import fileExistsCaseInsensitive from '../utils/findFileCaseInsensitive';
import StyleguidistError from '../utils/error';
import * as consts from '../consts';
import * as Rsg from '../../typings';
const EXTENSIONS = 'js,jsx,ts,tsx';
const DEFAULT_COMPONENTS_PATTERN =
// HACK: on windows, the case insensitivity makes each component appear twice
// to avoid this issue, the case management is removed on win32
// it virtually changes nothing
process.platform === 'win32'
? /* istanbul ignore next: no windows on our test plan */ `src/components/**/*.{${EXTENSIONS}}`
: `src/@(components|Components)/**/*.{${EXTENSIONS}}`;
const logger = glogg('rsg');
type NestedThemeValue = Record | string;
export type StyleguidistConfigKey = keyof Rsg.SanitizedStyleguidistConfig;
export interface ConfigSchemaOptions {
process?(value: any, config: T, rootDir: string): any;
default?: any;
required?: boolean | ((config?: T) => string | boolean);
deprecated?: string;
removed?: string;
type?: string | string[];
example?: any;
}
const configSchema: Record> = {
assetsDir: {
type: ['array', 'existing directory path'],
example: 'assets',
},
tocMode: {
type: 'string',
default: 'expand',
},
compilerConfig: {
type: 'object',
default: {
// Don't include an Object.assign ponyfill, we have our own
objectAssign: 'Object.assign',
// Transpile only features needed for IE11
target: { ie: 11 },
transforms: {
// Don't throw on ESM imports, we transpile them ourselves
modules: false,
// Enable tagged template literals for styled-components
dangerousTaggedTemplateString: true,
// to make async/await work by default (no transformation)
asyncAwait: false,
},
} as TransformOptions,
},
// `components` is a shortcut for { sections: [{ components }] },
// see `sections` below
components: {
type: ['string', 'function', 'array'],
example: 'components/**/[A-Z]*.js',
},
configDir: {
process: (value: string, config: Rsg.StyleguidistConfig, rootDir: string): string => rootDir,
},
context: {
type: 'object',
default: {},
example: {
map: 'lodash/map',
},
},
contextDependencies: {
type: 'array',
},
configureServer: {
type: 'function',
},
dangerouslyUpdateWebpackConfig: {
type: 'function',
},
defaultExample: {
type: ['boolean', 'existing file path'],
default: false,
process: (val: boolean | string): string | boolean =>
val === true ? path.resolve(__dirname, '../../../templates/DefaultExample.md') : val,
},
exampleMode: {
type: 'string',
process: (value: string, config: Rsg.StyleguidistConfig): string => {
return config.showCode === undefined ? value : config.showCode ? 'expand' : 'collapse';
},
default: 'collapse',
},
getComponentPathLine: {
type: 'function',
default: (componentPath: string): string => componentPath,
},
getExampleFilename: {
type: 'function',
default: (componentPath: string): string | boolean => {
const files = [
path.join(path.dirname(componentPath), 'Readme.md'),
// ComponentName.md
componentPath.replace(path.extname(componentPath), '.md'),
// FolderName.md when component definition file is index.js
path.join(path.dirname(componentPath), path.basename(path.dirname(componentPath)) + '.md'),
];
for (const file of files) {
const existingFile = fileExistsCaseInsensitive(file);
if (existingFile) {
return existingFile;
}
}
return false;
},
},
handlers: {
type: 'function',
default: (componentPath: string): reactDocgen.Handler[] =>
reactDocgen.defaultHandlers.concat(createDisplayNameHandler(componentPath)),
},
ignore: {
type: 'array',
default: [
'**/__tests__/**',
`**/*.test.{${EXTENSIONS}}`,
`**/*.spec.{${EXTENSIONS}}`,
'**/*.d.ts',
],
},
editorConfig: {
process: (value?: unknown): void => {
if (value) {
throw new StyleguidistError(
`${kleur.bold(
'editorConfig'
)} config option was removed. Use “theme” option to change syntax highlighting.`
);
}
},
},
logger: {
type: 'object',
},
minimize: {
type: 'boolean',
default: true,
},
moduleAliases: {
type: 'object',
},
mountPointId: {
type: 'string',
default: 'rsg-root',
},
pagePerSection: {
type: 'boolean',
default: false,
},
previewDelay: {
type: 'number',
default: 500,
},
printBuildInstructions: {
type: 'function',
},
printServerInstructions: {
type: 'function',
},
propsParser: {
type: 'function',
},
require: {
type: 'array',
default: [],
example: ['babel-polyfill', 'path/to/styles.css'],
},
resolver: {
type: 'function',
default: (
ast: ASTNode,
recast: {
visit: (
node: NodePath,
handlers: { [handlerName: string]: () => boolean | undefined }
) => void;
}
) => {
const findAllExportedComponentDefinitions =
reactDocgen.resolver.findAllExportedComponentDefinitions;
const annotatedComponents = annotationResolver(ast, recast);
const exportedComponents = findAllExportedComponentDefinitions(ast, recast);
return annotatedComponents.concat(exportedComponents);
},
},
ribbon: {
type: 'object',
example: {
url: 'http://example.com/',
text: 'Fork me on GitHub',
},
},
sections: {
type: 'array',
default: [],
process: (val: Rsg.ConfigSection[], config: Rsg.StyleguidistConfig): Rsg.ConfigSection[] => {
if (!val) {
// If root `components` isn't empty, make it a first section
// If `components` and `sections` weren’t specified, use default pattern
const components = config.components || DEFAULT_COMPONENTS_PATTERN;
return [
{
components,
},
];
}
return val;
},
example: [
{
name: 'Documentation',
content: 'Readme.md',
},
{
name: 'Components',
components: './lib/components/**/[A-Z]*.js',
},
],
},
serverHost: {
type: 'string',
default: '0.0.0.0',
},
serverPort: {
type: 'number',
default: parseInt(process.env.NODE_PORT as string) || 6060,
},
showCode: {
type: 'boolean',
default: false,
deprecated: 'Use exampleMode option instead',
},
showUsage: {
type: 'boolean',
default: false,
deprecated: 'Use usageMode option instead',
},
showSidebar: {
type: 'boolean',
default: true,
},
skipComponentsWithoutExample: {
type: 'boolean',
default: false,
},
sortProps: {
type: 'function',
},
styleguideComponents: {
type: 'object',
},
styleguideDir: {
type: 'directory path',
default: 'styleguide',
},
styles: {
type: ['object', 'existing file path', 'function'],
default: {},
example: {
Logo: {
logo: {
fontStyle: 'italic',
},
},
},
process: (val: NestedThemeValue, config: unknown, configDir: string): NestedThemeValue => {
return typeof val === 'string' ? path.resolve(configDir, val) : val;
},
},
template: {
type: ['object', 'function'],
default: {},
process: (val: any) => {
if (typeof val === 'string') {
throw new StyleguidistError(
`${kleur.bold(
'template'
)} config option format has been changed, you need to update your config.`,
'template'
);
}
return val;
},
},
theme: {
type: ['object', 'existing file path'],
default: {},
example: {
link: 'firebrick',
linkHover: 'salmon',
},
process: (val: NestedThemeValue, config: unknown, configDir: string): NestedThemeValue =>
typeof val === 'string' ? path.resolve(configDir, val) : val,
},
title: {
type: 'string',
process: (val?: string): string => {
if (val) {
return val;
}
const name = getUserPackageJson().name || '';
return `${startCase(name)} Style Guide`;
},
example: 'My Style Guide',
},
updateDocs: {
type: 'function',
},
updateExample: {
type: 'function',
default: (props: { lang: string }): { lang: string } => {
if (props.lang === 'example') {
props.lang = 'js';
logger.warn(
'"example" code block language is deprecated. Use "js", "jsx" or "javascript" instead:\n' +
consts.DOCS_DOCUMENTING
);
}
return props;
},
},
updateWebpackConfig: {
type: 'function',
removed: `Use "webpackConfig" option instead:\n${consts.DOCS_WEBPACK}`,
},
usageMode: {
type: 'string',
process: (value: string, config: Rsg.StyleguidistConfig) => {
return config.showUsage === undefined ? value : config.showUsage ? 'expand' : 'collapse';
},
default: 'collapse',
},
verbose: {
type: 'boolean',
default: false,
},
version: {
type: 'string',
},
webpackConfig: {
type: ['object', 'function'],
process: (val?: any) => {
if (val) {
return val;
}
const file = findUserWebpackConfig();
if (file) {
logger.info(`Loading webpack config from:\n${file}`);
// eslint-disable-next-line import/no-dynamic-require
return require(file);
}
logger.warn(
'No webpack config found. ' +
'You may need to specify "webpackConfig" option in your style guide config:\n' +
consts.DOCS_WEBPACK
);
return undefined;
},
example: {
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
loader: 'babel-loader',
},
],
},
},
},
};
export default configSchema;
================================================
FILE: src/scripts/server.ts
================================================
import WebpackDevServer from 'webpack-dev-server';
import webpack from 'webpack';
import createServer from './create-server';
import * as Rsg from '../typings';
export default function server(
config: Rsg.SanitizedStyleguidistConfig,
callback: (error?: Error) => void
): { app: WebpackDevServer; compiler: webpack.Compiler } {
const env = 'development';
const serverInfo = createServer(config, env);
serverInfo.app.startCallback(callback);
return serverInfo;
}
================================================
FILE: src/scripts/utils/StyleguidistOptionsPlugin.ts
================================================
import webpack, { Compilation, Compiler, WebpackPluginInstance, LoaderContext } from 'webpack';
import * as Rsg from '../../typings';
// Webpack plugin that makes Styleguidist config available for Styleguidist webpack loaders.
// It will be available as `this._styleguidist`.
//
// Other working in webpack 2 way is to use LoaderOptionsPlugin, but it has problems.
// See this issue for details: https://github.com/styleguidist/react-styleguidist/issues/328
export default class StyleguidistOptionsPlugin implements WebpackPluginInstance {
private options: Rsg.SanitizedStyleguidistConfig;
public constructor(options: Rsg.SanitizedStyleguidistConfig) {
this.options = options;
}
private pluginFunc = (
context: Rsg.StyleguidistLoaderContext,
module: LoaderContext
) => {
if (!module.resource) {
return;
}
context._styleguidist = this.options;
};
/**
*
* @param compil Compilation
*/
private plugin = (compil: Compilation) => {
webpack.NormalModule.getCompilationHooks(compil).loader.tap(
'StyleguidistOptionsPlugin',
this.pluginFunc as any
);
};
public apply(compiler: Compiler) {
compiler.hooks.compilation.tap('StyleguidistOptionsPlugin', this.plugin);
}
}
================================================
FILE: src/scripts/utils/__tests__/StyleguidistOptionsPlugin.spec.ts
================================================
import StyleguidistOptionsPlugin from '../StyleguidistOptionsPlugin';
import * as Rsg from '../../../typings';
const options: any = {
foo: 42,
};
const mockContext: { _styleguidist?: Rsg.StyleguidistConfig } = {};
let mockedModule: Record;
jest.mock('webpack', () => {
return {
NormalModule: {
getCompilationHooks: () => {
return {
loader: {
tap: (moduleName: string, compilationCallback: (context: any, opt: any) => void) => {
compilationCallback(mockContext, mockedModule);
},
},
};
},
},
};
});
it('should do nothing when module.resource is not present', () => {
mockedModule = {};
const compiler = {
hooks: {
compilation: {
tap: (name: string, callback: (opt: any) => void) => {
callback({});
},
},
},
};
const plugin = new StyleguidistOptionsPlugin(options);
plugin.apply(compiler as any);
expect(mockContext._styleguidist).toBeFalsy();
});
it('should attach Styleguidist config options', () => {
mockedModule = { resource: 'test' };
const compiler = {
hooks: {
compilation: {
tap: (name: string, callback: (opt: any) => void) => {
callback({});
},
},
},
};
const plugin = new StyleguidistOptionsPlugin(options);
plugin.apply(compiler as any);
expect(mockContext._styleguidist).toEqual(options);
});
================================================
FILE: src/scripts/utils/__tests__/findFileCaseInsensitive.spec.ts
================================================
import path from 'path';
import findFileCaseInsensitive, { clearCache } from '../findFileCaseInsensitive';
it('should return a file path with the correct case if a file exists', () => {
const result = findFileCaseInsensitive(path.join(__dirname, 'Findfilecaseinsensitive.Spec.TS'));
expect(result).toMatch(__filename);
});
it('should return undefined if a file doesn’t exist', () => {
const result = findFileCaseInsensitive(path.join(__dirname, 'pizza.js'));
expect(result).toBeFalsy();
});
it('cache clean function shouldn’t throw', () => {
const fn = () => clearCache();
expect(fn).not.toThrowError();
});
================================================
FILE: src/scripts/utils/__tests__/findUserWebpackConfig.spec.ts
================================================
import findUserWebpackConfig from '../findUserWebpackConfig';
const cwd = process.cwd();
afterEach(() => process.chdir(cwd));
it('should return path to Create React App Webpack old config (react-scripts <= 2.1.1)', () => {
const result = findUserWebpackConfig(a => a);
expect(result).toMatchInlineSnapshot(`"react-scripts/config/webpack.config.dev"`);
});
it('should return path to Create React App Webpack config (react-scripts > 2.1.1)', () => {
const result = findUserWebpackConfig(a => {
if (/webpack\.config\.dev/.test(a)) {
// Simulate an error. For example, if the file doesn't exist.
throw new Error();
}
return a;
});
expect(result).toMatchInlineSnapshot(`"react-scripts/config/webpack.config"`);
});
it('should return an absolute path to user Webpack config located in project root folder', () => {
process.chdir('test/apps/basic');
const result = findUserWebpackConfig();
expect(result).toMatch(/^\//);
expect(result).toMatch(/webpack.config.js$/);
});
it('should return false if there is no webpack config', () => {
process.chdir('test/apps/no-webpack');
const result = findUserWebpackConfig();
expect(result).toBeFalsy();
});
================================================
FILE: src/scripts/utils/__tests__/getUserPackageJson.spec.ts
================================================
import getUserPackageJson from '../getUserPackageJson';
const cwd = process.cwd();
afterEach(() => {
process.chdir(cwd);
});
it('should return object with package.json contents', () => {
process.chdir('test/apps/cra');
const result = getUserPackageJson();
expect(result).toBeTruthy();
expect(result.name).toBe('pizza-cra');
});
================================================
FILE: src/scripts/utils/__tests__/getWebpackVersion.spec.ts
================================================
import getWebpackVersion from '../getWebpackVersion';
it('should return version number', () => {
const result = getWebpackVersion();
expect(result).toBeGreaterThanOrEqual(1);
});
================================================
FILE: src/scripts/utils/__tests__/mergeWebpackConfig.spec.ts
================================================
import mergeWebpackConfig from '../mergeWebpackConfig';
class TerserPlugin {
public apply() {}
}
class MyPlugin {
public apply() {}
}
class MiniHtmlWebpackPlugin {
public apply() {}
}
it('should merge two objects', () => {
const result = mergeWebpackConfig(
{ entry: 'main.js', devtool: 'cheap-source-map' },
{ devtool: 'inline-source-map' }
);
expect(result).toEqual({ entry: 'main.js', devtool: 'cheap-source-map' });
});
it('should merge an object and a function', () => {
const result = mergeWebpackConfig({ entry: 'main.js', devtool: 'cheap-source-map' }, () => ({
devtool: 'inline-source-map',
}));
expect(result).toEqual({ entry: 'main.js', devtool: 'cheap-source-map' });
});
it('should pass an environment to a user config', () => {
const env = 'production';
const userConfig = jest.fn();
mergeWebpackConfig({}, userConfig, env);
expect(userConfig).toBeCalledWith(env);
});
it('should ignore certain sections', () => {
const result = mergeWebpackConfig({ entry: 'main' }, () => ({
entry: 'other',
module: { rules: [] },
}));
expect(result).toEqual({ entry: 'main', module: { rules: [] } });
});
it('should ignore certain Webpack plugins', done => {
const baseInstance = new TerserPlugin();
const userInstance = new TerserPlugin();
const result = mergeWebpackConfig(
{
plugins: [baseInstance],
},
{
plugins: [userInstance, new MyPlugin(), new MiniHtmlWebpackPlugin()],
}
);
// this test is necessary as some results can contain no plugins
if (!result || !result.plugins) {
done.fail();
return;
}
expect(result.plugins).toHaveLength(2);
expect(result.plugins[0]).toBe(baseInstance);
expect(result.plugins[0].constructor.name).toBe('TerserPlugin');
expect(result.plugins[1].constructor.name).toBe('MyPlugin');
done();
});
it('should pass devtool settings in development', () => {
const result = mergeWebpackConfig(
{ devtool: false },
() => ({ devtool: 'source-map' }),
'development'
);
expect(result).toEqual({ devtool: 'source-map' });
});
it('should ignore devtool settings in production', () => {
const result = mergeWebpackConfig(
{ devtool: false },
() => ({ devtool: 'source-map' }),
'production'
);
expect(result).toEqual({ devtool: false });
});
================================================
FILE: src/scripts/utils/__tests__/sanitizeConfig.spec.ts
================================================
import path from 'path';
import glogg from 'glogg';
import sanitizeConfig from '../sanitizeConfig';
const logger = glogg('rsg');
it('should return non-empty required field as is', () => {
const result = sanitizeConfig(
{
food: 'pizza',
},
{
food: {
required: true,
},
},
''
);
expect(result).toBeTruthy();
expect(result.food).toBe('pizza');
});
it('should return default value for empty non-required field', () => {
const result = sanitizeConfig<{ food?: string }>(
{},
{
food: {
default: 'pizza',
},
},
''
);
expect(result.food).toBe('pizza');
});
it('should return actual value for non-empty field with default value', () => {
const result = sanitizeConfig(
{
food: 'burger',
},
{
food: {
default: 'pizza',
},
},
''
);
expect(result.food).toBe('burger');
});
it('should accept required as a function', () => {
const result = sanitizeConfig(
{
food: 'pizza',
},
{
food: {
required: () => true,
},
},
''
);
expect(result.food).toBe('pizza');
});
it('should throw if required field is undefined', () => {
const fn = () =>
sanitizeConfig(
{},
{
food: {
required: true,
},
},
''
);
expect(fn).toThrowError('config option is required');
});
it('should throw with custom message returned by required function', () => {
const fn = () =>
sanitizeConfig(
{},
{
food: {
required: () => 'Not good',
},
},
''
);
expect(fn).toThrowError('Not good');
});
it('should throw when type in schema is incorrect', () => {
const fn = () =>
sanitizeConfig(
{
food: 42,
},
{
food: {
type: 'pizza',
},
},
''
);
expect(fn).toThrowError('Wrong type');
});
it('should check type for number', () => {
const result = sanitizeConfig(
{
food: 42,
},
{
food: {
type: 'number',
},
},
''
);
expect(result.food).toBe(42);
});
it('should throw when field is not a number', () => {
const fn = () =>
sanitizeConfig(
{
food: 'pizza',
},
{
food: {
type: 'number',
},
},
''
);
expect(fn).toThrowError('config option should be');
});
it('should check type for string', () => {
const result = sanitizeConfig(
{
food: 'pizza',
},
{
food: {
type: 'string',
},
},
''
);
expect(result.food).toBe('pizza');
});
it('should throw when field is not a string', () => {
const fn = () =>
sanitizeConfig(
{
food: 42,
},
{
food: {
type: 'string',
},
},
''
);
expect(fn).toThrowError('config option should be');
});
it('should check type for boolean', () => {
const result = sanitizeConfig(
{
food: true,
},
{
food: {
type: 'boolean',
},
},
''
);
expect(result.food).toBe(true);
});
it('should throw when field is not a boolean', () => {
const fn = () =>
sanitizeConfig(
{
food: 42,
},
{
food: {
type: 'boolean',
},
},
''
);
expect(fn).toThrowError('config option should be');
});
it('should check type for array', () => {
const result = sanitizeConfig(
{
food: [1, 2],
},
{
food: {
type: 'array',
},
},
''
);
expect(result.food).toEqual([1, 2]);
});
it('should throw when field is not an array', () => {
const fn = () =>
sanitizeConfig(
{
food: 42,
},
{
food: {
type: 'array',
},
},
''
);
expect(fn).toThrowError('config option should be');
});
it('should check type for function', () => {
const result = sanitizeConfig(
{
food: () => true,
},
{
food: {
type: 'function',
},
},
''
);
expect(typeof result.food).toBe('function');
});
it('should throw when field is not a function', () => {
const fn = () =>
sanitizeConfig(
{
food: 42,
},
{
food: {
type: 'function',
},
},
''
);
expect(fn).toThrowError('config option should be');
});
it('should check type for object', () => {
const result = sanitizeConfig(
{
food: { a: 42 },
},
{
food: {
type: 'object',
},
},
''
);
expect(result.food).toEqual({ a: 42 });
});
it('should throw when field is not an object', () => {
const fn = () =>
sanitizeConfig(
{
food: 42,
},
{
food: {
type: 'object',
},
},
''
);
expect(fn).toThrowError('config option should be');
});
it('should check type for file path', () => {
const result = sanitizeConfig(
{
food: __filename,
},
{
food: {
type: 'file path',
},
},
__dirname
);
expect(result.food).toEqual(__filename);
});
it('should check type for relative file path and absolutize it', () => {
const result = sanitizeConfig(
{
food: path.basename(__filename),
},
{
food: {
type: 'file path',
},
},
__dirname
);
expect(result.food).toEqual(__filename);
});
it('should throw when file does not exist', () => {
const fn = () =>
sanitizeConfig(
{
food: 'pizza.js',
},
{
food: {
type: 'existing file path',
},
},
__dirname
);
expect(fn).toThrowError('does not exist');
});
it('should check type for directory path', () => {
const result = sanitizeConfig(
{
food: __dirname,
},
{
food: {
type: 'directory path',
},
},
__dirname
);
expect(result.food).toEqual(__dirname);
});
it('should check type for relative directory path and absolutize it', () => {
const result = sanitizeConfig(
{
food: 'data',
},
{
food: {
type: 'file path',
},
},
__dirname
);
expect(result.food).toEqual(path.join(__dirname, 'data'));
});
it('should throw with correct type name', () => {
const fn = () =>
sanitizeConfig(
{
food: null,
},
{
food: {
type: 'object',
},
},
''
);
expect(fn).toThrowError('config option should be object, received null');
});
it('should pass value to a custom process function', () => {
const result = sanitizeConfig(
{
food: true,
},
{
food: {
type: ['boolean', 'string'],
process: val => (val === true ? 'pizza' : val),
},
},
''
);
expect(result.food).toEqual('pizza');
});
it('should not throw if process function returns value for undefined required field', () => {
const fn = () =>
sanitizeConfig(
{},
{
food: {
required: true,
process: () => 'pizza',
},
},
''
);
expect(fn).not.toThrowError('config option is required');
});
it('should throw when directory does not exist', () => {
const fn = () =>
sanitizeConfig(
{
food: 'pizza.js',
},
{
food: {
type: 'existing directory path',
},
},
__dirname
);
expect(fn).toThrowError('does not exist');
});
it('should throw for unknown options', () => {
const fn = () =>
sanitizeConfig<{ drink?: any; food?: any }>(
{
book: 'hobbit',
} as any,
{
drink: {},
food: {},
},
''
);
expect(fn).toThrowError('Unknown config option');
});
it('should throw for unknown options with suggestion', () => {
const fn = () =>
sanitizeConfig<{ drink?: any; food?: any }>(
{
dring: 'pizza',
} as any,
{
drink: {},
food: {},
},
''
);
expect(fn).toThrowError('Did you mean');
});
it('should warn for deprecated options', () => {
const warn = jest.fn();
logger.once('warn', warn);
const result = sanitizeConfig(
{
food: 'pizza',
},
{
food: {
deprecated: 'Don’t use!',
},
},
''
);
expect(result.food).toBe('pizza');
expect(warn).toBeCalledWith(expect.stringMatching('config option is deprecated. Don’t use!'));
});
it('should throw for removed options', () => {
const fn = () =>
sanitizeConfig(
{
food: 'pizza',
},
{
food: {
removed: 'Don’t use!',
},
},
''
);
expect(fn).toThrowError('was removed');
});
================================================
FILE: src/scripts/utils/ensureWebpack.ts
================================================
/**
* Check webpack availability and version at run time instead of using peerDependencies to allow
* usage of build tools that contains webpack as their own dependency, like Create React App.
*/
import getWebpackVersion from './getWebpackVersion';
import StyleguidistError from './error';
import * as consts from '../consts';
const MIN_WEBPACK_VERSION = 4;
const webpackVersion = getWebpackVersion();
if (!webpackVersion) {
throw new StyleguidistError(
'Webpack is required for Styleguidist, please add it to your project:\n\n' +
' npm install --save-dev webpack\n\n' +
'See how to configure webpack for your style guide:\n' +
consts.DOCS_WEBPACK
);
} else if (webpackVersion < MIN_WEBPACK_VERSION) {
throw new StyleguidistError(
`Webpack ${webpackVersion} is not supported by Styleguidist, the minimum supported version is ${MIN_WEBPACK_VERSION}`
);
}
================================================
FILE: src/scripts/utils/error.ts
================================================
class StyleguidistError extends Error {
public extra: any;
public constructor(message: string, extra?: any) {
super(message);
Error.captureStackTrace(this, this.constructor);
Object.defineProperty(this, 'name', {
value: this.constructor.name,
});
Object.defineProperty(this, 'extra', {
value: extra,
});
}
}
export default StyleguidistError;
================================================
FILE: src/scripts/utils/findFileCaseInsensitive.ts
================================================
import fs from 'fs';
import path from 'path';
import memoize from 'lodash/memoize';
const readdirSync = memoize(fs.readdirSync);
/**
* Find a file in a directory, case-insensitive
*
* @param {string} filepath
* @return {string|undefined} File path with correct case
*/
export default function findFileCaseInsensitive(filepath: string): string | undefined {
const dir = path.dirname(filepath);
const fileNameLower = path.basename(filepath).toLowerCase();
const files = readdirSync(dir);
const found = files.find(file => file.toLowerCase() === fileNameLower);
return found && path.join(dir, found);
}
/**
* Clear cache.
*/
export function clearCache() {
(readdirSync.cache as any).clear();
}
================================================
FILE: src/scripts/utils/findUserWebpackConfig.ts
================================================
import fs from 'fs';
import path from 'path';
// react-scripts <= 2.1.1
const CREATE_REACT_APP_WEBPACK_CONFIG_OLD = 'react-scripts/config/webpack.config.dev';
// react-scripts > 2.1.1
const CREATE_REACT_APP_WEBPACK_CONFIG = 'react-scripts/config/webpack.config';
const USER_WEBPACK_CONFIG_NAMES = ['webpack.config.js', 'webpackfile.js'];
const absolutize = (filePath: string): string => path.resolve(process.cwd(), filePath);
/**
* Find user’s Webpack config and return its path.
* Fixed location for Create React App or webpack.config.js in the root directory.
* Returns false if config not found.
*
* @param {Function} resolve
* @return {string|boolean}
*/
export default function findUserWebpackConfig(resolve?: (input: string) => string) {
resolve = resolve || require.resolve;
try {
// Create React App <= 2.1.1
return resolve(CREATE_REACT_APP_WEBPACK_CONFIG_OLD);
} catch (err) {
try {
// Create React App > 2.1.1
return resolve(CREATE_REACT_APP_WEBPACK_CONFIG);
} catch (innerErr) {
// Check in the root folder
// FIXME: This looks like a bug in ESLint
// eslint-disable-next-line no-unused-vars
for (const configFile of USER_WEBPACK_CONFIG_NAMES) {
const absoluteConfigFile = absolutize(configFile);
if (fs.existsSync(absoluteConfigFile)) {
return absoluteConfigFile;
}
}
}
}
return false;
}
================================================
FILE: src/scripts/utils/getUserPackageJson.ts
================================================
import path from 'path';
/**
* Return user’s package.json.
*
* @return {object}
*/
export default function getUserPackageJson() {
try {
return require(path.resolve(process.cwd(), 'package.json'));
} catch (err) {
return {};
}
}
================================================
FILE: src/scripts/utils/getWebpackVersion.ts
================================================
/**
* Return installed Webpack version.
*
* @return {number}
*/
export default function getWebpackVersion() {
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
return parseInt(require('webpack/package.json').version, 10);
} catch (err) {
return undefined;
}
}
================================================
FILE: src/scripts/utils/mergeWebpackConfig.ts
================================================
import mergeBase from 'webpack-merge';
import isFunction from 'lodash/isFunction';
import omit from 'lodash/omit';
import { Configuration, WebpackPluginInstance } from 'webpack';
const IGNORE_SECTIONS = ['entry', 'externals', 'output', 'watch', 'stats', 'styleguidist'];
const IGNORE_SECTIONS_ENV: Record = {
development: [],
// For production builds, we'll ignore devtool settings to avoid
// source mapping bloat.
production: ['devtool'],
};
const IGNORE_PLUGINS = [
'CommonsChunkPlugins',
'MiniHtmlWebpackPlugin',
'HtmlWebpackPlugin',
'OccurrenceOrderPlugin',
'DedupePlugin',
'UglifyJsPlugin',
'TerserPlugin',
'HotModuleReplacementPlugin',
];
const merge = mergeBase({
// Ignore user’s plugins to avoid duplicates and issues with our plugins
customizeArray: mergeBase.unique(
'plugins',
IGNORE_PLUGINS,
(plugin: WebpackPluginInstance) => plugin.constructor && plugin.constructor.name
),
});
type MetaConfig = Configuration | ((env?: string) => Configuration);
/**
* Merge two Webpack configs.
*
* In the user config:
* - Ignores given sections (options.ignore).
* - Ignores plugins that shouldn’t be used twice or may cause issues.
*
* @param {object} baseConfig
* @param {object|Function} userConfig
* @param {string} env
* @return {object}
*/
export default function mergeWebpackConfig(
baseConfig: MetaConfig,
userConfig: MetaConfig,
env = 'production'
) {
const userConfigObject = isFunction(userConfig) ? userConfig(env) : userConfig;
const safeUserConfig = omit(userConfigObject, IGNORE_SECTIONS.concat(IGNORE_SECTIONS_ENV[env]));
return merge(baseConfig, safeUserConfig);
}
================================================
FILE: src/scripts/utils/sanitizeConfig.ts
================================================
import fs from 'fs';
import path from 'path';
import castArray from 'lodash/castArray';
import isBoolean from 'lodash/isBoolean';
import isFunction from 'lodash/isFunction';
import isPlainObject from 'lodash/isPlainObject';
import isString from 'lodash/isString';
import isFinite from 'lodash/isFinite';
import map from 'lodash/map';
import listify from 'listify';
import kleur from 'kleur';
import { distance } from 'fastest-levenshtein';
import typeDetect from 'type-detect';
import loggerMaker from 'glogg';
import { stringify } from 'q-i';
import StyleguidistError from './error';
import { ConfigSchemaOptions } from '../schemas/config';
const logger = loggerMaker('rsg');
const typeCheckers: Record boolean> = {
number: isFinite,
string: isString,
boolean: isBoolean,
array: Array.isArray,
function: isFunction,
object: isPlainObject,
'file path': isString,
'existing file path': isString,
'directory path': isString,
'existing directory path': isString,
};
const typesList = (types: string[]) => listify(types, { finalWord: 'or' });
const shouldBeFile = (types: string[]) => types.some((type) => type.includes('file'));
const shouldBeDirectory = (types: string[]) => types.some((type) => type.includes('directory'));
const shouldExist = (types: string[]) => types.some((type) => type.includes('existing'));
function isDirectory(pathString: string): boolean {
try {
return fs.lstatSync(pathString).isDirectory();
} catch (e: any) {
if (e.code !== 'ENOENT') {
throw e;
}
return false;
}
}
/**
* Validates and normalizes config.
*
* @param {object} config
* @param {object} schema
* @param {string} rootDir
* @return {object}
*/
export default function sanitizeConfig>(
config: T,
schema: Record>,
rootDir: string
): T {
// Check for unknown fields
map(config, (value, keyAny: keyof T) => {
const key = keyAny as string;
if (!schema[key]) {
// Try to guess
const possibleOptions = Object.keys(schema);
const suggestedOption = possibleOptions.reduce((suggestion: string, option: string) => {
const steps = distance(option, key);
if (steps < 2) {
return option;
}
return suggestion;
}, '');
throw new StyleguidistError(
`Unknown config option ${kleur.bold(key)} was found, the value is:\n` +
stringify(value) +
(suggestedOption ? `\n\nDid you mean ${kleur.bold(suggestedOption)}?` : ''),
suggestedOption
);
}
});
// Check all fields
const safeConfig: Partial = {};
map(schema, (props, keyAny: keyof T) => {
const key = keyAny as string;
let value = config[key];
// Custom processing
if (props.process) {
value = props.process(value, config, rootDir);
}
if (value === undefined) {
// Default value
value = props.default;
// Check if the field is required
const isRequired = isFunction(props.required) ? props.required(config) : props.required;
if (isRequired) {
const message = isString(isRequired)
? isRequired
: `${kleur.bold(key)} config option is required.`;
throw new StyleguidistError(message, key);
}
} else if (props.deprecated) {
logger.warn(`${key} config option is deprecated. ${props.deprecated}`);
} else if (props.removed) {
throw new StyleguidistError(`${kleur.bold(key)} config option was removed. ${props.removed}`);
}
if (value !== undefined && props.type) {
const types = castArray(props.type);
// Check type
const hasRightType = types.some((type) => {
if (!typeCheckers[type]) {
throw new StyleguidistError(
`Wrong type ${kleur.bold(type)} specified for ${kleur.bold(key)} in schema.`
);
}
return typeCheckers[type](value);
});
if (!hasRightType) {
const exampleValue = props.example || props.default;
const example: Record = {};
if (exampleValue) {
example[key] = exampleValue;
}
const exampleText = exampleValue
? `
Example:
${stringify(example)}`
: '';
throw new StyleguidistError(
`${kleur.bold(key)} config option should be ${typesList(types)}, received ${typeDetect(
value
)}.\n${exampleText}`,
key
);
}
// Absolutize paths
if (isString(value) && (shouldBeFile(types) || shouldBeDirectory(types))) {
value = path.resolve(rootDir, value);
// Check for existence
if (shouldExist(types)) {
if (shouldBeFile(types) && !fs.existsSync(value)) {
throw new StyleguidistError(
`A file specified in ${kleur.bold(key)} config option does not exist:\n${value}`,
key
);
}
if (shouldBeDirectory(types) && !isDirectory(value)) {
throw new StyleguidistError(
`A directory specified in ${kleur.bold(key)} config option does not exist:\n${value}`,
key
);
}
}
}
}
safeConfig[keyAny] = value;
});
return safeConfig as T;
}
================================================
FILE: src/typings/RecursivePartial.ts
================================================
/**
* In a custom config file you might only want to override some parameters
* This is the usage of the recursive Partial
* `interface Test{param:string, paramObject:{p1:number, p2:boolean}}`
* becomes
* `interface TestPartial{param?:string, paramObject?:{p1?:number, p2?:boolean}}`
* where everything is optional
*/
export type RecursivePartial = {
[P in keyof T]?: T[P] extends (infer U)[]
? RecursivePartial[]
: T[P] extends (...args: unknown[]) => unknown
? T[P]
: T[P] extends Record
? RecursivePartial
: T[P];
};
================================================
FILE: src/typings/RsgComponent.ts
================================================
import { MethodDescriptor, PropDescriptor, TagProps } from 'react-docgen';
import { RequireItResult } from './RsgRequireItResult';
import { Example } from './RsgExample';
export type ExpandMode = 'expand' | 'collapse' | 'hide';
export interface BaseComponent {
hasExamples?: boolean;
name?: string;
slug?: string;
href?: string;
filepath?: string;
pathLine?: string;
description?: string;
exampleMode?: ExpandMode;
usageMode?: ExpandMode;
}
export interface Component extends BaseComponent {
visibleName?: string;
props?: {
displayName?: string;
visibleName?: string;
description?: string;
methods?: MethodDescriptor[];
props?: PropDescriptor[];
tags?: TagProps;
example?: Example[];
examples?: Example[];
};
module?: number;
metadata?: {
tags?: string[];
};
}
export interface LoaderComponent extends BaseComponent {
module: RequireItResult;
props: RequireItResult;
metadata: RequireItResult | Record;
}
================================================
FILE: src/typings/RsgExample.ts
================================================
export interface MarkdownExample {
type: 'markdown';
content: string;
settings?: Record;
}
export interface CodeExample {
type: 'code';
content: string;
lang?: string | null;
settings?: Record;
}
export interface RuntimeCodeExample extends CodeExample {
evalInContext(a: string): () => any;
}
export type Example = RuntimeCodeExample | MarkdownExample;
================================================
FILE: src/typings/RsgPropsObject.ts
================================================
import { DocumentationObject, MethodDescriptor, PropDescriptor } from 'react-docgen';
import { RequireItResult } from './RsgRequireItResult';
export interface MethodWithDocblock extends MethodDescriptor {
docblock: string;
}
export interface TempPropsObject extends DocumentationObject {
displayName: string;
visibleName?: string;
methods?: MethodWithDocblock[];
doclets: Record;
example?: RequireItResult | null;
}
export interface PropsObject extends Omit {
props?: Record | PropDescriptor[];
examples?: RequireItResult | null;
}
================================================
FILE: src/typings/RsgRequireItResult.ts
================================================
import { ASTNode } from 'ast-types';
export interface RequireItResult {
require: string;
toAST(): ASTNode;
}
================================================
FILE: src/typings/RsgSection.ts
================================================
import { RequireItResult } from './RsgRequireItResult';
import { MarkdownExample, Example } from './RsgExample';
import { LoaderComponent, ExpandMode, Component } from './RsgComponent';
export interface BaseSection {
name?: string;
slug?: string;
ignore?: string | string[];
description?: string;
exampleMode?: ExpandMode;
usageMode?: ExpandMode;
href?: string;
sectionDepth?: number;
external?: boolean;
expand?: boolean;
}
export interface ProcessedSection extends BaseSection {
visibleName?: string;
filepath?: string;
externalLink?: boolean;
href?: string;
}
/**
* Section used on the client in javascript
* It is the output of the function `client/utils/processSection`
*/
export interface Section extends ProcessedSection {
content?: Example[] | string;
components?: Component[];
sections?: Section[];
}
/**
* Item of the Table Of Contents used in
* ComponentsList
* TableOfContent
* filterSectionByName
*/
export interface TOCItem extends ProcessedSection {
heading?: boolean;
shouldOpenInNewTab?: boolean;
selected?: boolean;
initialOpen?: boolean;
forcedOpen?: boolean;
content?: React.ReactNode;
components?: TOCItem[];
sections?: TOCItem[];
}
/**
* Used in the config file and at the early stages of processing
* in `schema/config.ts` this is the type that is used
*/
export interface ConfigSection extends BaseSection {
components?: string | string[] | (() => string[]);
sections?: ConfigSection[];
content?: string;
}
/**
* Type returned when sections are transformed to their webpack
* loadable equivalents
*/
export interface LoaderSection extends BaseSection {
slug?: string;
content?: RequireItResult | MarkdownExample;
components: LoaderComponent[];
sections: LoaderSection[];
}
================================================
FILE: src/typings/RsgStyleguidistConfig.ts
================================================
import WebpackDevServer from 'webpack-dev-server';
import { Configuration, LoaderContext } from 'webpack';
import { TransformOptions } from 'buble';
import { Handler, DocumentationObject, PropDescriptor } from 'react-docgen';
import { ASTNode } from 'ast-types';
import { NodePath } from 'ast-types/lib/node-path';
import { Styles } from 'jss';
import { RecursivePartial } from './RecursivePartial';
import { ExpandMode } from './RsgComponent';
import { PropsObject } from './RsgPropsObject';
import { CodeExample } from './RsgExample';
import { ConfigSection, Section } from './RsgSection';
import { Theme } from './RsgTheme';
type OptionsType = {
displayName: string;
file: string;
shouldShowDefaultExample: string;
customLangs: string[];
};
export interface StyleguidistLoaderContext extends LoaderContext {
_styleguidist: SanitizedStyleguidistConfig;
}
interface BaseStyleguidistConfig {
assetsDir: string | string[];
tocMode: ExpandMode;
compilerConfig: TransformOptions;
components: (() => string[]) | string | string[];
configDir: string;
context: Record;
contextDependencies: string[];
configureServer(server: WebpackDevServer, env: string): string;
dangerouslyUpdateWebpackConfig: (server: Configuration, env: string) => Configuration;
defaultExample: string | false;
exampleMode: ExpandMode;
editorConfig: {
theme: string;
};
getComponentPathLine(componentPath: string): string;
getExampleFilename(componentPath: string): string;
handlers: (componentPath: string) => Handler[];
ignore: string[];
logger: {
info(message: string): void;
warn(message: string): void;
debug(message: string): void;
};
minimize: boolean;
mountPointId: string;
moduleAliases: Record;
pagePerSection: boolean;
previewDelay: number;
printBuildInstructions(config: SanitizedStyleguidistConfig): void;
printServerInstructions(config: SanitizedStyleguidistConfig, options: { isHttps: boolean }): void;
propsParser(
filePath: string,
code: string,
resolver: (
ast: ASTNode,
parser: { parse: (input: string) => ASTNode }
) => NodePath | NodePath[],
handlers: Handler[]
): DocumentationObject;
require: string[];
resolver(
ast: ASTNode,
parser: { parse: (code: string) => ASTNode }
): NodePath | NodePath[];
ribbon?: {
text?: string;
url: string;
};
serverHost: string;
serverPort: number;
showCode: boolean;
showUsage: boolean;
showSidebar: boolean;
skipComponentsWithoutExample: boolean;
sortProps(props: PropDescriptor[]): PropDescriptor[];
styleguideComponents: Record;
styleguideDir: string;
styles: Styles | string | ((theme: Theme) => Styles);
template: any;
theme: RecursivePartial | string;
title: string;
updateDocs(doc: PropsObject, file: string): PropsObject;
updateExample(props: Omit, ressourcePath: string): Omit;
updateWebpackConfig(config: Configuration): Configuration;
usageMode: ExpandMode;
verbose: boolean;
version: string;
webpackConfig: Configuration | ((env?: string) => Configuration);
}
export interface ProcessedStyleguidistConfig extends BaseStyleguidistConfig {
sections: Section[];
theme: RecursivePartial;
styles: ((th: Theme) => Styles) | Styles;
}
export type ProcessedStyleguidistCSSConfig = Pick &
Pick;
export interface SanitizedStyleguidistConfig extends BaseStyleguidistConfig {
sections: ConfigSection[];
}
/**
* definition of the config object where everything is optional
* note that teh default example can be both a string and a boolean but ends
* up only being a string after sanitizing
*/
export interface StyleguidistConfig
extends RecursivePartial> {
defaultExample?: string | boolean;
}
================================================
FILE: src/typings/RsgTheme.ts
================================================
/**
* When the theme is to be used in a component,
* it will have all it's values set.
* None of those declarations should be optional
*/
export interface Theme {
spaceFactor: number;
space: number[];
color: {
base: string;
light: string;
lightest: string;
link: string;
linkHover: string;
focus: string;
border: string;
name: string;
type: string;
error: string;
baseBackground: string;
codeBackground: string;
sidebarBackground: string;
ribbonBackground: string;
ribbonText: string;
// Based on default Prism theme
codeBase: string;
codeComment: string;
codePunctuation: string;
codeProperty: string;
codeDeleted: string;
codeString: string;
codeInserted: string;
codeOperator: string;
codeKeyword: string;
codeFunction: string;
codeVariable: string;
};
fontFamily: {
base: string[];
monospace: string[];
};
fontSize: {
base: number;
text: number;
small: number;
h1: number;
h2: number;
h3: number;
h4: number;
h5: number;
h6: number;
};
mq: {
small: string;
};
borderRadius: number;
maxWidth: number;
sidebarWidth: number;
buttonTextTransform: string;
}
================================================
FILE: src/typings/dependencies/acorn-jsx.ts
================================================
declare module 'acorn-jsx' {
import { Parser } from 'acorn';
function acornJsx(): (BaseParser: typeof Parser) => typeof Parser;
export = acornJsx;
}
================================================
FILE: src/typings/dependencies/common-dir.ts
================================================
declare module 'common-dir' {
function commonDir(list: string[]): string;
export = commonDir;
}
================================================
FILE: src/typings/dependencies/deabsdeep.ts
================================================
declare module 'deabsdeep' {
interface Options {
root?: string;
mask?: string;
}
function deabsdeep(objectToFreze: T, opt?: Options): T;
export = deabsdeep;
}
================================================
FILE: src/typings/dependencies/deepfreeze.ts
================================================
declare module 'deepfreeze' {
function deepfreeze(objectToFreze: T): T;
export = deepfreeze;
}
================================================
FILE: src/typings/dependencies/findup.ts
================================================
declare module 'findup' {
const findup: {
sync(cwd: string, path: string): string;
};
export = findup;
}
================================================
FILE: src/typings/dependencies/github-slugger.ts
================================================
declare module 'github-slugger' {
class Slugger {
reset(): void;
slug(input: string): string;
}
export = Slugger;
}
================================================
FILE: src/typings/dependencies/glogg.ts
================================================
declare module 'glogg' {
interface GloggLogger {
debug(msg: string): void;
info(msg: string): void;
warn(msg: string): void;
error(msg: string): void;
on(event: string | symbol, listener: (...args: any[]) => void): void;
once(actionName: string, action: (msg: string) => void): void;
removeAllListeners(): void;
}
function getLogger(namespace: string): GloggLogger;
export = getLogger;
}
================================================
FILE: src/typings/dependencies/listify.ts
================================================
declare module 'listify' {
interface ListifyOptions {
finalWord: string;
}
function listify(list: any[], opt?: ListifyOptions): string;
export = listify;
}
================================================
FILE: src/typings/dependencies/mini-html-webpack-template.ts
================================================
declare module '@vxna/mini-html-webpack-template' {
function template(...args: any[]): string;
export = template;
}
================================================
FILE: src/typings/dependencies/q-i.ts
================================================
declare module 'q-i' {
export const stringify: (obj: any) => string;
}
================================================
FILE: src/typings/dependencies/react-docgen.ts
================================================
declare module 'react-docgen' {
import { Tag, Type } from 'doctrine';
import { ASTNode } from 'ast-types';
import { NodePath } from 'ast-types/lib/node-path';
export type Handler = (documentation: Documentation, path: NodePath) => void;
interface Documentation {
addComposes(moduleName: string): void;
set(key: string, value: any): void;
get(key: string): any;
getPropDescriptor(propName: string): PropDescriptor;
getContextDescriptor(propName: string): PropDescriptor;
getChildContextDescriptor(propName: string): PropDescriptor;
toObject(): DocumentationObject;
}
export interface TagObject extends Omit {
description?: string;
}
export interface TagParamObject extends TagObject {
name: string;
type?: Type | null;
default?: string;
}
export interface TagProps {
deprecated?: TagObject[];
see?: TagObject[];
link?: TagObject[];
author?: TagObject[];
version?: TagObject[];
since?: TagObject[];
returns?: TagParamObject[];
return?: TagParamObject[];
arg?: TagParamObject[];
argument?: TagParamObject[];
param?: TagParamObject[];
[title: string]: TagObject[] | undefined;
}
export interface PropTypeDescriptor {
name:
| 'arrayOf'
| 'custom'
| 'enum'
| 'array'
| 'bool'
| 'func'
| 'number'
| 'object'
| 'string'
| 'any'
| 'element'
| 'node'
| 'symbol'
| 'objectOf'
| 'shape'
| 'exact'
| 'instanceOf'
| 'elementType';
value?: any;
raw?: string;
computed?: boolean;
// These are only needed for shape/exact types.
// Consider consolidating PropTypeDescriptor and PropDescriptor
description?: string;
required?: boolean;
}
export interface PropDescriptor {
name: string;
type?: PropTypeDescriptor;
required?: boolean;
defaultValue?: any;
description?: string;
tags?: TagProps;
}
export interface MethodDescriptor {
name: string;
description?: string;
docblock?: string;
returns?: { name: string; [key: string]: any } | null;
params?: any[];
modifiers?: string[];
tags?: TagProps;
}
export interface DocumentationObject {
displayName?: string;
description?: string;
tags?: TagProps;
props?: { [propName: string]: PropDescriptor };
methods?: MethodDescriptor[];
context?: { [constextName: string]: PropDescriptor };
childContext?: { [chilCOntextName: string]: PropDescriptor };
composes?: string[];
}
interface Options {
filename?: string;
cwd?: string;
babelrc?: string;
babelrcRoots?: boolean | string | string[];
root?: string;
rootMode?: 'root' | 'upward' | 'upward-optional';
configFile?: string;
envName?: string;
}
export const defaultHandlers: Handler[];
/**
* Parse the components at filePath and return props, public methods, events and slots
* @param filePath absolute path of the parsed file
* @param opts
*/
export function parse(
source: string | Buffer,
resolver?: (
ast: ASTNode,
parser: { parse: (code: string) => ASTNode }
) => NodePath | NodePath[],
handlers?: Handler[],
options?: Options
): DocumentationObject | DocumentationObject[];
export const utils: {
docblock: {
getDoclets: (str?: string) => Record;
};
};
export const resolver: {
findAllComponentDefinitions(ast: ASTNode): NodePath[];
findAllExportedComponentDefinitions(
ast: ASTNode,
recast: {
visit: (
path: NodePath,
handlers: { [handlerName: string]: () => boolean | undefined }
) => void;
}
): NodePath[];
findExportedComponentDefinition(ast: ASTNode): NodePath | undefined;
};
}
declare module 'react-docgen-displayname-handler' {
import { NodePath as DisplaNameHandlerNodePath } from 'ast-types/lib/node-path';
import { Documentation } from 'react-docgen';
type Handler = (documentation: Documentation, path: DisplaNameHandlerNodePath) => void;
export function createDisplayNameHandler(componentPath: string): Handler;
}
declare module 'react-docgen-annotation-resolver' {
import { ASTNode as AnnoASTNode } from 'ast-types';
import { NodePath as AnnoNodePath } from 'ast-types/lib/node-path';
function annotationResolver(
ast: AnnoASTNode,
recast: {
visit: (
node: AnnoNodePath,
handlers: { [handlerName: string]: () => boolean | undefined }
) => void;
}
): AnnoNodePath[];
export = annotationResolver;
}
================================================
FILE: src/typings/dependencies/strip-shebang.ts
================================================
declare module 'strip-shebang' {
function stripShebang(input: string): string;
export = stripShebang;
}
================================================
FILE: src/typings/dependencies/stripHtmlComments.ts
================================================
declare module 'strip-html-comments' {
function stripHtmlComments(text: string): string;
export = stripHtmlComments;
}
================================================
FILE: src/typings/dependencies/to-ast.ts
================================================
declare module 'to-ast' {
import { ASTNode } from 'ast-types';
function toAST(obj: any): ASTNode;
export = toAST;
}
================================================
FILE: src/typings/dependencies/webpack-merge.ts
================================================
declare module 'webpack-merge' {
import { Configuration, WebpackPluginInstance } from 'webpack';
type MetaConfig = Configuration | ((env?: string) => Configuration);
type mergeFunction = (...configs: MetaConfig[]) => Configuration;
type customizeArrayFuntion = () => any[];
interface WebpackMergeOptions {
customizeArray: customizeArrayFuntion;
}
const webpackMerge: {
(options: WebpackMergeOptions): mergeFunction;
(...configs: MetaConfig[]): Configuration;
unique(
key: string,
uniques: string[],
getter?: (plugin: WebpackPluginInstance) => string | undefined | false
): customizeArrayFuntion;
};
export = webpackMerge;
}
================================================
FILE: src/typings/index.ts
================================================
import './dependencies/acorn-jsx';
import './dependencies/findup';
import './dependencies/listify';
import './dependencies/react-docgen';
import './dependencies/webpack-merge';
import './dependencies/common-dir';
import './dependencies/github-slugger';
import './dependencies/strip-shebang';
import './dependencies/deabsdeep';
import './dependencies/glogg';
import './dependencies/mini-html-webpack-template';
import './dependencies/stripHtmlComments';
import './dependencies/deepfreeze';
import './dependencies/q-i';
import './dependencies/to-ast';
export * from './RsgComponent';
export * from './RsgExample';
export * from './RsgPropsObject';
export * from './RsgRequireItResult';
export * from './RsgSection';
export * from './RsgStyleguidistConfig';
export * from './RsgTheme';
================================================
FILE: src/typings/test.Classes.d.ts
================================================
import { Theme } from './RsgTheme';
declare global {
/**
* function used in react tests to generate
* mocks of JSS Class names
*/
const classes: (styles: (theme: Theme) => Record) => Record;
}
================================================
FILE: styleguide.config.js
================================================
module.exports = {
components: 'src/client/rsg-components/**/[A-Z]*.js',
webpackConfig: {
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
loader: 'babel-loader',
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
},
};
================================================
FILE: templates/DefaultExample.md
================================================
<__COMPONENT__>Default Example Usage
================================================
FILE: test/apps/basic/package.json
================================================
{
"name": "pizza-basic",
"devDependencies": {
"postcss-loader": "^7.0.0"
}
}
================================================
FILE: test/apps/basic/styleguide.config.js
================================================
const path = require('path');
const dir = path.resolve(__dirname, 'lib');
module.exports = {
title: 'React Style Guide Example',
defaultExample: true,
components: './components/**/[A-Z]*.js',
webpackConfig: {
module: {
rules: [
{
test: /\.jsx?$/,
include: dir,
loader: 'babel-loader',
},
{
test: /\.css$/,
include: dir,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
},
},
],
},
],
},
},
};
================================================
FILE: test/apps/basic/webpack.config.js
================================================
module.exports = {
output: 'nope.js',
resolve: {
extensions: ['.scss'],
},
};
================================================
FILE: test/apps/cra/package.json
================================================
{
"name": "pizza-cra",
"devDependencies": {
"postcss-loader": "^7.0.0",
"react-scripts": "^5.0.0"
}
}
================================================
FILE: test/apps/defaults/package.json
================================================
{
"name": "Pizza"
}
================================================
FILE: test/apps/defaults/src/components/Button.js
================================================
================================================
FILE: test/apps/defaults/src/components/Button.md
================================================
================================================
FILE: test/apps/defaults/src/components/Placeholder.js
================================================
================================================
FILE: test/apps/defaults/styleguide.config.js
================================================
module.exports = {};
================================================
FILE: test/apps/no-webpack/package.json
================================================
{
"name": "pizza-no-webpack",
"devDependencies": {}
}
================================================
FILE: test/apps/no-webpack/styleguide.config.js
================================================
module.exports = {
title: 'React Style Guide Example',
components: './components/**/[A-Z]*.js',
};
================================================
FILE: test/browser.js
================================================
/* eslint-disable no-console */
/* eslint-disable import/no-extraneous-dependencies */
const puppeteer = require('puppeteer');
const path = require('path');
const args = process.argv.slice(2);
let browser;
process.on('unhandledRejection', (reason) => {
console.log('Unhandled Promise rejection:', reason);
if (browser) {
browser.close().then(() => process.exit(1));
}
process.exit(1);
});
async function onerror(err) {
console.error(err.stack);
if (browser) {
await browser.close();
}
process.exit(1);
}
(async () => {
browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] });
const page = await browser.newPage();
await page.setViewport({ width: 1024, height: 768 });
page.on('error', onerror);
page.on('pageerror', onerror);
page.on('console', (msg) => {
if (msg.type() !== 'clear') {
console.log('PAGE LOG:', msg.text());
}
});
const url = /https?/.test(args[0]) ? args[0] : `file://${path.resolve(args[0])}`;
await page.goto(url);
if (args[1]) {
await page.screenshot({ path: args[1] });
}
await browser.close();
})().catch(onerror);
================================================
FILE: test/classes.js
================================================
import keymirror from 'keymirror';
import * as theme from '../src/client/styles/theme';
export default (styles) => keymirror(styles(theme));
================================================
FILE: test/components/.eslintrc
================================================
{
"parser": "babel-eslint",
"extends": "tamia/react",
"rules": {
"valid-jsdoc": 0
}
}
================================================
FILE: test/components/Annotation/Annotation.js
================================================
/* eslint-disable */
import styled from 'styled-components';
/**
* @component
* Styled-component test
* */
export default styled('div')`
display: inline;
`;
================================================
FILE: test/components/Button/Button.js
================================================
import React from 'react';
import PropTypes from 'prop-types';
/**
* The only true button.
*/
export default function Button({ color, size, children }) {
const styles = {
color,
fontSize: Button.sizes[size],
};
return {children} ;
}
Button.propTypes = {
/**
* Button label.
*/
children: PropTypes.string.isRequired,
color: PropTypes.string,
size: PropTypes.oneOf(['small', 'normal', 'large']),
/**
* A prop that should not be visible in the doc.
* @ignore
*/
ignoredProp: PropTypes.bool,
};
Button.defaultProps = {
color: '#333',
size: 'normal',
};
Button.sizes = {
small: '10px',
normal: '14px',
large: '18px',
};
================================================
FILE: test/components/Button/Readme.md
================================================
Basic button:
Push Me
Big pink button:
Click Me
And you _can_ **use** `any` [Markdown](http://daringfireball.net/projects/markdown/) here.
If you define a fenced code block with a language flag it will be rendered as a regular Markdown code snippet:
```javascript
import React from 'react'
```
================================================
FILE: test/components/Label/Label.md
================================================
Basic Label:
Hi there !!!
Pink background label:
Click Me
================================================
FILE: test/components/Label/index.js
================================================
import React from 'react';
import PropTypes from 'prop-types';
/**
* The only true label.
*/
export default function Label({ color, background, children }) {
const styles = {
color,
background,
padding: '.5em 1em',
borderRadius: '0.3em',
fontFamily: 'arial',
};
// eslint-disable-next-line jsx-a11y/label-has-for
return {children} ;
}
Label.propTypes = {
/**
* Label text.
*/
children: PropTypes.string.isRequired,
color: PropTypes.string,
background: PropTypes.string,
};
Label.defaultProps = {
color: '#333',
background: 'white',
};
================================================
FILE: test/components/Placeholder/Placeholder.js
================================================
import React, { Component } from 'react';
import PropTypes from 'prop-types';
/**
* Image placeholders.
*
* @example ./examples.md
* @see {@link link}
* @link link
*/
export default class Placeholder extends Component {
static propTypes = {
type: PropTypes.oneOf([
'animal',
'bacon',
'beard',
'bear',
'cat',
'food',
'city',
'nature',
'people',
]),
width: PropTypes.number,
height: PropTypes.number,
alt: PropTypes.string,
};
static defaultProps = {
type: 'animal',
width: 150,
height: 150,
alt: 'Photo of an animal',
};
/**
* A public method.
* @public
*/
getImageUrl() {
const { type, width, height } = this.props;
const types = {
animal: `http://placeimg.com/${width}/${height}/animals`,
bacon: `http://baconmockup.com/${width}/${height}`,
bear: `http://www.placebear.com/${width}/${height}`,
beard: `http://placebeard.it/${width}/${height}`,
cat: `http://lorempixel.com/${width}/${height}/cats`,
city: `http://lorempixel.com/${width}/${height}/city`,
food: `http://lorempixel.com/${width}/${height}/food`,
nature: `http://lorempixel.com/${width}/${height}/nature`,
people: `http://lorempixel.com/${width}/${height}/people`,
};
return types[type];
}
makeABarrelRoll() {
return 'This is a private method';
}
render() {
const { width, height, alt } = this.props;
return (
);
}
}
================================================
FILE: test/components/Placeholder/Placeholder.json
================================================
{
"customMetadata": "This is some sample custom metadata",
"anotherCustomMetadata": "This is some another custom metadata",
"foo": true,
"bar": false
}
================================================
FILE: test/components/Placeholder/Placeholder.md
================================================
================================================
FILE: test/components/Placeholder/examples.md
================================================
Hello world!
================================================
FILE: test/components/Price/Price.js
================================================
import React from 'react';
import PropTypes from 'prop-types';
const unitSymbols = {
USD: '$',
EUR: '€',
};
/**
* Price component that renders a price and a unit.
*/
export default function Price(props) {
let Host = 'span';
if (props.emphasize) {
Host = 'em';
}
return (
{props.value}
{!props.symbol ? props.unit : unitSymbols[props.unit]}
);
}
Price.propTypes = {
/** Price value. */
value: PropTypes.number.isRequired,
/** Price unit */
unit: PropTypes.oneOf(['EUR', 'USD']),
/** Flag that determines if the price should be emphasized or not. */
emphasize: PropTypes.bool,
/** Defines if the unit should be shown as a symbol or not. */
symbol: PropTypes.bool.isRequired,
};
================================================
FILE: test/components/RandomButton/RandomButton.js
================================================
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import sample from 'lodash/sample';
/**
* Button that changes label on every click.
*/
export default class RandomButton extends Component {
static propTypes = {
/**
* List of possible labels.
*/
variants: PropTypes.array.isRequired,
};
constructor(props) {
super();
this.state = {
label: sample(props.variants),
};
}
handleClick = () => {
this.setState({
label: sample(this.props.variants),
});
};
render() {
return {this.state.label} ;
}
}
================================================
FILE: test/cypress/.eslintrc
================================================
{
"env": {
"mocha": true
}
}
================================================
FILE: test/cypress/fixtures/example.json
================================================
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}
================================================
FILE: test/cypress/integration/component_spec.js
================================================
describe('Single component', () => {
before(() => {
// Open simple button component in isolation
cy.visit('/#!/Button');
});
describe('props and methods section', () => {
beforeEach(() => {
cy.get('button').contains('Props & methods').as('propsBtn');
cy.get('@propsBtn').closest('[class^=rsg--tabs]').as('container');
});
it('is present', () => {
cy.get('@propsBtn').should('exist');
});
it('does not show table initially', () => {
cy.get('@container').find('table').should('not.exist');
});
it('shows the table on button click', () => {
cy.get('@propsBtn').click();
cy.get('@container').find('table').should('contain', 'Prop name');
});
});
describe('preview section', () => {
beforeEach(() => {
cy.get('[data-testid*="-example-"]')
.as('container')
.find('[class^=rsg--preview]')
.as('preview');
cy.get('@container').find('button').contains('View Code').as('viewCodeBtn');
});
it('renders component preview', () => {
cy.get('@preview').find('button', { timeout: 10000 }).contains('Push Me').should('exist');
});
it('has view code button', () => {
cy.get('@viewCodeBtn').should('exist');
});
it('does not show code initially', () => {
cy.get('@container').find('textarea').should('not.exist');
});
it('shows code on click', () => {
cy.get('@viewCodeBtn').click();
cy.get('@container').find('textarea').should('exist');
});
it('changes the render after code change', () => {
const codeToSkip = '';
cy.get('@container')
.find('textarea')
.type(`${'{leftarrow}'.repeat(codeToSkip.length)} Harder`);
cy.get('@preview').find('button').contains('Push Me Harder').should('exist');
});
it('toggles isolated example mode correctly', () => {
cy.get('[data-testid$="-examples"]').as('componentExamples');
// Toggle into isolated example mode
cy.get('@componentExamples').find('[data-testid$="-isolate-button"]').first().click();
// Assert that there is only one example showing
cy.get('@componentExamples').find('[data-testid*="-example-"]').should('have.length', 1);
// Toggle out of isolated example mode
cy.get('[data-testid$="-isolate-button"]').click();
// Assert the other examples are showing again
cy.get('@componentExamples')
.find('[data-testid*="-example-"]')
.should('have.length.above', 1);
// Check that we've returned to isolated component mode instead of normal mode
// TODO: this is currently bugged (returns to normal mode rather than isolated component mode)
//cy.get('[id$=container]').should('have.length', 1);
});
});
});
================================================
FILE: test/cypress/integration/core_spec.js
================================================
describe('Styleguidist core', () => {
before(() => cy.visit('/'));
it('loads the page', () => {
cy.title().should('include', 'React Styleguidist');
});
it('shows multiple components in normal mode', () => {
cy.get('[data-testid$=-container]').should('have.length.above', 1);
});
it('toggles isolated component mode correctly', () => {
cy.get('[data-testid=sidebar]').as('sidebar');
// Toggle into isolated mode
cy.get('[data-testid$="-isolate-button"]').first().click();
// Assert there's only one component showing
cy.get('[data-testid$=-container]').should('have.length', 1);
// Assert the sidebar is no longer showing
cy.get('@sidebar').should('not.exist');
// Toogle out of isolated mode
cy.get('[data-testid$="-isolate-button"]').first().click();
// Assert that more than one component is now showing
cy.get('[data-testid$=-container]').should('have.length.above', 1);
// Asser that the sidebar is now showing again
cy.get('@sidebar').should('exist');
});
});
================================================
FILE: test/cypress/plugins/index.js
================================================
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
module.exports = (/* on, config */) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
};
================================================
FILE: test/cypress/support/commands.js
================================================
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
================================================
FILE: test/cypress/support/index.js
================================================
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
// import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')
================================================
FILE: test/data/badconfig.config.js
================================================
module.exports = {
title: 'React Style Guide Example',
// No components or sections: one of these fields is required
};
================================================
FILE: test/data/styleguide.config.js
================================================
const path = require('path');
const dir = path.resolve(__dirname, 'lib');
module.exports = {
title: 'React Style Guide Example',
defaultExample: true,
components: './components/**/[A-Z]*.js',
webpackConfig: {
module: {
rules: [
{
test: /\.jsx?$/,
include: dir,
loader: 'babel-loader',
},
{
test: /\.css$/,
include: dir,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
},
},
],
},
],
},
},
};
================================================
FILE: test/data/webpack.config.func.js
================================================
module.exports = (env) => ({
output: 'nope.js',
resolve: {
extensions: [env],
},
});
================================================
FILE: test/data/webpack.config.js
================================================
module.exports = {
output: 'nope.js',
resolve: {
extensions: ['.scss'],
},
};
================================================
FILE: test/deabsdeepSerializer.js
================================================
const escape = require('escape-string-regexp');
const isObject = require('is-plain-obj');
const path = require('path');
const MASK = '~';
const DIRNAME = getRootDir();
/**
* Recursively replace absolute paths in object keys and values or in array values with a “~”.
*
* @param {object} obj
* @param {object} [options]
* @param {string} [options.root]
* @param {string} [options.mask]
* @return {object}
*/
function deabsDeep(obj, options) {
options = options || {};
const root = options.root || DIRNAME;
const mask = options.mask || MASK;
const regExp = new RegExp(escape(root), 'g');
const deabs = (s) => (typeof s === 'string' ? s.replace(regExp, mask) : s);
if (Array.isArray(obj)) {
return obj.map(deabs);
}
return mapObj(obj, (key, value) => [deabs(key), deabs(value)]);
}
function getRootDir(dir) {
dir = dir || __dirname;
const m = dir.match(/[\\/]node_modules[\\/]/);
return m ? dir.substring(0, m.index) : path.resolve(__dirname, '..');
}
/* istanbul ignore next */
function mapObj(obj, fn, seen) {
seen = seen || new WeakMap();
if (seen.has(obj)) {
return seen.get(obj);
}
const target = {};
seen.set(obj, target);
for (const key of Object.keys(obj)) {
const val = obj[key];
const res = fn(key, val, obj);
let newVal = res[1];
if (isObject(newVal)) {
if (Array.isArray(newVal)) {
newVal = newVal.map((x) => (isObject(x) ? mapObj(x, fn, seen) : x));
} else {
newVal = mapObj(newVal, fn, seen);
}
}
target[res[0]] = newVal;
}
// The $$typeof property is a React marker that's used for serialization.
// deabsdeep doesn't know about React Elements but since it's registered globally,
// it should keep this property on the object.
if (obj.$$typeof) {
target.$$typeof = obj.$$typeof;
}
return target;
}
// Borrowed from https://github.com/eyolas/jest-serializer-supertest
const KEY = '__JEST_SERIALIZER_DEABSDEEP__';
module.exports = {
test(val) {
return (Array.isArray(val) || isObject(val)) && !Object.prototype.hasOwnProperty.call(val, KEY);
},
print(val, serialize) {
const newVal = deabsDeep(val);
// To skip maximum call stack size exceeded
Object.defineProperty(newVal, KEY, {
enumerable: false,
});
return serialize(newVal);
},
};
================================================
FILE: test/empty.js
================================================
module.exports = '';
================================================
FILE: test/jestsetup.js
================================================
/* eslint-disable no-console */
import keymirror from 'keymirror';
import * as theme from '../src/client/styles/theme';
// Get class names from styles function
global.classes = (styles) => keymirror(styles(theme));
jest.mock('react-scripts/config/webpack.config.dev', () => ({ cra: true }), { virtual: true });
jest.mock('webpack-dev-server', function () {
return function () {
return {
app: {},
};
};
});
================================================
FILE: test/raf-polyfill.js
================================================
// requestAnimationFrame “polyfill”
window.requestAnimationFrame = (a) => a();
global.requestAnimationFrame = window.requestAnimationFrame;
================================================
FILE: test/run.build.js
================================================
/* eslint-disable no-console */
const styleguidist = require('../lib/scripts');
styleguidist(require('../examples/basic/styleguide.config.js')).build((err, config) => {
if (err) {
console.log(err);
} else {
console.log('Style guide published to', config.styleguideDir);
}
});
================================================
FILE: test/run.server.js
================================================
/* eslint-disable no-console */
const path = require('path');
const styleguidist = require('../lib/scripts');
const dir = path.resolve(__dirname, '../examples/basic/src');
styleguidist({
components: path.resolve(dir, 'components/**/[A-Z]*.js'),
webpackConfig: {
module: {
rules: [
{
test: /\.jsx?$/,
include: dir,
loader: 'babel-loader',
},
{
test: /\.css$/,
include: dir,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
},
},
],
},
],
},
},
moduleAliases: {
'rsg-example': dir,
},
logger: {
info: console.log,
warn: (message) => console.warn(`Warning: ${message}`),
},
serverPort: 8082,
// Do not require delays in integration tests
previewDelay: 0,
}).server((err, config) => {
if (err) {
console.log(err);
} else {
console.log('Listening at http://' + config.serverHost + ':' + config.serverPort);
}
});
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"target": "esnext",
"module": "es2015",
"baseUrl": ".",
"moduleResolution": "node",
"jsx": "react",
"strict": true,
"esModuleInterop": true,
"noUnusedLocals": true,
"allowSyntheticDefaultImports": true,
"outDir": "./lib",
"lib": ["dom"],
"skipLibCheck": true,
"paths": {
"rsg-components/*": ["src/client/rsg-components/*"]
}
},
"files": ["dangerfile.ts"],
"include": ["src"],
"exclude": ["node_modules"]
}
================================================
FILE: tsconfig.types.json
================================================
{
"extends": "./tsconfig.json",
"compilerOptions": {
// only generate typings when using this tsconfig
"declaration": true
},
// ignore the dangerfile in calculation of the root folder
"files": [],
// avoid generating typings for tests and mocks
"exclude": [
"dangerfile.ts",
"node_modules",
"**/__tests__/*.ts",
"**/__mocks__/*.ts",
"**/*.spec.tsx",
"**/*.spec.ts"
]
}