Merge branch 'next' into add-angular-fixtures

This commit is contained in:
Kai Röder 2019-11-19 21:37:07 +01:00
commit 3760140fcd
663 changed files with 10913 additions and 6721 deletions

View File

@ -13,7 +13,10 @@ const withTests = {
};
module.exports = {
ignore: ['./lib/codemod/src/transforms/__testfixtures__'],
ignore: [
'./lib/codemod/src/transforms/__testfixtures__',
'./lib/postinstall/src/__testfixtures__',
],
presets: [
['@babel/preset-env', { shippedProposals: true, useBuiltIns: 'usage', corejs: '3' }],
'@babel/preset-typescript',

View File

@ -1,25 +1,21 @@
const ignore = 0;
// const warn = 1;
const error = 2;
module.exports = {
root: true,
extends: ['@storybook/eslint-config-storybook'],
rules: {
'import/extensions': [
error,
'error',
'never',
{ ignorePackages: true, md: 'always', svg: 'always', json: 'always', tag: 'always' },
],
'import/no-unresolved': [error, { ignore: ['@storybook'] }],
'react/state-in-constructor': ignore,
'react/static-property-placement': ignore,
'react/jsx-props-no-spreading': ignore,
'react/jsx-fragments': ignore,
'@typescript-eslint/ban-ts-ignore': ignore,
'@typescript-eslint/no-object-literal-type-assertion': ignore,
'import/no-unresolved': ['error', { ignore: ['@storybook'] }],
'react/state-in-constructor': 'off',
'react/static-property-placement': 'off',
'react/jsx-props-no-spreading': 'off',
'react/jsx-fragments': 'off',
'@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/no-object-literal-type-assertion': 'off',
'react/sort-comp': [
error,
'error',
{
order: [
'staticLifecycle',
@ -38,7 +34,7 @@ module.exports = {
},
},
],
'max-classes-per-file': ignore,
'max-classes-per-file': 'off',
},
overrides: [
{
@ -51,30 +47,30 @@ module.exports = {
'docs/src/stories/**',
],
rules: {
'@typescript-eslint/no-empty-function': ignore,
'import/no-extraneous-dependencies': ignore,
'@typescript-eslint/no-empty-function': 'off',
'import/no-extraneous-dependencies': 'off',
},
},
{ files: '**/.storybook/config.js', rules: { 'global-require': ignore } },
{ files: '**/.storybook/config.js', rules: { 'global-require': 'off' } },
{
files: ['**/*.stories.*'],
rules: {
'no-console': ignore,
'no-console': 'off',
},
},
{
files: ['**/*.tsx', '**/*.ts'],
rules: {
'react/prop-types': ignore, // we should use types
'no-dupe-class-members': ignore, // this is called overloads in typescript
'react/prop-types': 'off', // we should use types
'no-dupe-class-members': 'off', // this is called overloads in typescript
},
},
{
files: ['**/*.d.ts'],
rules: {
'vars-on-top': ignore,
'no-var': ignore, // this is how typescript works
'spaced-comment': ignore,
'vars-on-top': 'off',
'no-var': 'off', // this is how typescript works
'spaced-comment': 'off',
},
},
],

View File

@ -1,3 +1,148 @@
## 5.3.0-beta.2 (November 19, 2019)
### Features
* Addon-docs: Customizable DocPage doc blocks ([#8855](https://github.com/storybookjs/storybook/pull/8855))
### Bug Fixes
* Addon-docs: Add back Props "exclude" support ([#8868](https://github.com/storybookjs/storybook/pull/8868))
* Addon-docs: Fix MDX component permalinking ([#8872](https://github.com/storybookjs/storybook/pull/8872))
* Addon-docs: Fix regression to @ignore in Props ([#8867](https://github.com/storybookjs/storybook/pull/8867))
### Maintenance
* Addon-docs: Add tests for prop types default value ([#8869](https://github.com/storybookjs/storybook/pull/8869))
## 5.3.0-beta.1 (November 18, 2019)
### Features
* Addon-google-analytics: Add gaOption config ([#8859](https://github.com/storybookjs/storybook/pull/8859))
### Bug Fixes
* Addon-docs: Fix props table props sorting for PropTypes ([#8857](https://github.com/storybookjs/storybook/pull/8857))
* Fix layout of Preview container ([#8628](https://github.com/storybookjs/storybook/pull/8628))
## 5.3.0-beta.0 (November 16, 2019)
Storybook 5.3 is in beta y'all 🔥🔥🔥 It includes:
- 📝 Longform documentation in MDX
- 🎨 Multi-framework SB Docs (React, Vue, Angular, WC)
- 📦 Web-components framework support
- 🔼 Tri-config (experimental)
See the [latest changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md) for a full list of changes.
## 5.3.0-alpha.47 (November 16, 2019)
### Features
* Triconfig: Declarative story definition in main.js ([#8748](https://github.com/storybookjs/storybook/pull/8748))
* Storyshots: Triconfig support ([#8765](https://github.com/storybookjs/storybook/pull/8765))
### Bug Fixes
* Addon-viewports: Fix missing TypeScript types ([#8848](https://github.com/storybookjs/storybook/pull/8848))
### Dependency Upgrades
* Dependency upgrades ([#8847](https://github.com/storybookjs/storybook/pull/8847))
## 5.3.0-alpha.46 (November 16, 2019)
### Features
* Core: allow uppercase path names in url query param ([#8516](https://github.com/storybookjs/storybook/pull/8516))
### Bug Fixes
* Core: Fix null version check bug ([#8806](https://github.com/storybookjs/storybook/pull/8806))
* Addon-notes: Fix anchor links ([#8132](https://github.com/storybookjs/storybook/pull/8132))
### Maintenance
* Refactor: remove useless variables from eslint config ([#8843](https://github.com/storybookjs/storybook/pull/8843))
### Dependency Upgrades
* Addon-docs: Replace `storybook-addon-vue-info` with `vue-docgen-loader` ([#8831](https://github.com/storybookjs/storybook/pull/8831))
## 5.3.0-alpha.45 (November 14, 2019)
### Breaking Changes
* CSF: Deprecate displayName parameter ([#8775](https://github.com/storybookjs/storybook/pull/8775))
### Features
* Addon-docs: Rich prop tables ([#8826](https://github.com/storybookjs/storybook/pull/8826))
* Core: Simplified hierarchy separators ([#8796](https://github.com/storybookjs/storybook/pull/8796))
* CLI: Upgrade hierarchy separator codemod + examples ([#8818](https://github.com/storybookjs/storybook/pull/8818))
* CLI: Addon postinstall hooks ([#8700](https://github.com/storybookjs/storybook/pull/8700))
* CSF/MDX: Add component id for permalinks ([#8808](https://github.com/storybookjs/storybook/pull/8808))
* Addon-knobs: Add object[] support for select ([#7957](https://github.com/storybookjs/storybook/pull/7957))
### Bug Fixes
* Addon-A11y: Show errors, reset config properly ([#8779](https://github.com/storybookjs/storybook/pull/8779))
## 5.3.0-alpha.44 (November 13, 2019)
### Features
* React-native: Add theming to ondevice-addons ([#8738](https://github.com/storybookjs/storybook/pull/8738))
### Bug Fixes
* UI: Store layout state in sessionStorage ([#8786](https://github.com/storybookjs/storybook/pull/8786))
* Core: Use `stable` package to ensure story sorting is stable ([#8795](https://github.com/storybookjs/storybook/pull/8795))
### Maintenance
* Svelte: Migrate @storybook/svelte to Typescript ([#8770](https://github.com/storybookjs/storybook/pull/8770))
## 5.3.0-alpha.43 (November 11, 2019)
### Bug Fixes
* Source-loader: Warn if applied to non-stories file ([#8773](https://github.com/storybookjs/storybook/pull/8773))
### Maintenance
* Presets / Addon-docs: Cleanup framework-specific presets ([#8782](https://github.com/storybookjs/storybook/pull/8782))
* Add @babel/runtime to workspace ([#8774](https://github.com/storybookjs/storybook/pull/8774))
## 5.2.6 (November 9, 2019)
### Bug Fixes
* Addon-info: Remove jsnext:main ([#8764](https://github.com/storybookjs/storybook/pull/8764))
* Addon-info: Fix "The prop 'children' is marked as required in 'Td'" ([#8745](https://github.com/storybookjs/storybook/pull/8745))
* UI: Fix unmount components on Canvas/Docs tab switch ([#8625](https://github.com/storybookjs/storybook/pull/8625))
* Addon-docs: Fix code style inside LI ([#8708](https://github.com/storybookjs/storybook/pull/8708))
* Remove min-height CSS rule from DocsPage wrapper ([#8366](https://github.com/storybookjs/storybook/pull/8366))
* Core: Revert webpack rebuild changes in node_modules ([#8657](https://github.com/storybookjs/storybook/pull/8657))
* Addon-notes: Add key to render function ([#8633](https://github.com/storybookjs/storybook/pull/8633))
* Addon-docs: Fix story scroll-to heuristics ([#8629](https://github.com/storybookjs/storybook/pull/8629))
* React-native-server: Changed default port to number in CLI options ([#8584](https://github.com/storybookjs/storybook/pull/8584))
* Increase TooltipLinkList max-height to accommodate more links ([#8545](https://github.com/storybookjs/storybook/pull/8545))
* Prevent form submission as search is done while typing ([#8546](https://github.com/storybookjs/storybook/pull/8546))
* Ondevice-knobs: Fix peer dep ([#8644](https://github.com/storybookjs/storybook/pull/8644))
## 5.3.0-alpha.42 (November 9, 2019)
### Bug Fixes
* Addon-info: Remove jsnext:main ([#8764](https://github.com/storybookjs/storybook/pull/8764))
* Addon-info: Fix "The prop 'children' is marked as required in 'Td'" ([#8745](https://github.com/storybookjs/storybook/pull/8745))
### Maintenance
* React-native: Update compilation target - it was compiled for old browsers ([#8698](https://github.com/storybookjs/storybook/pull/8698))
## 5.3.0-alpha.41 (November 7, 2019)
### Features

View File

@ -327,9 +327,7 @@ First we are going to install storybook, then we are going to link `@storybook/r
You should now have a working storybook dev environment up and running.
> TODO: update this section (is already incorrect)
Save and go to `http://localhost:9009` (or wherever storybook is running)
Save and go to `http://localhost:9011` (or wherever storybook is running)
If you don't see the changes rerun `yarn storybook` again in your sandbox app

View File

@ -5,6 +5,9 @@
- [Create React App preset](#create-react-app-preset)
- [Description doc block](#description-doc-block)
- [React Native Async Storage](#react-native-async-storage)
- [Deprecate displayName parameter](#deprecate-displayname-parameter)
- [Unified docs preset](#unified-docs-preset)
- [Simplified hierarchy separators](#simplified-hierarchy-separators)
- [From version 5.1.x to 5.2.x](#from-version-51x-to-52x)
- [Source-loader](#source-loader)
- [Default viewports](#default-viewports)
@ -97,17 +100,41 @@ To avoid that now you have to manually pass asyncStorage to React Native Storybo
Solution:
- Use `require('@react-native-community/async-storage')` for React Native v0.59 and above.
- Use `require('@react-native-community/async-storage').AsyncStorage` for React Native v0.59 and above.
- Use `require('react-native').AsyncStorage` for React Native v0.58 or below.
- Use `null` to disable Async Storage completely.
```javascript
getStorybookUI({
...
asyncStorage: require('@react-native-community/async-storage') || require('react-native').AsyncStorage || null
asyncStorage: require('@react-native-community/async-storage').AsyncStorage || require('react-native').AsyncStorage || null
});
```
### Deprecate displayName parameter
In 5.2, the story parameter `displayName` was introduced as a publicly visible (but internal) API. Storybook's Component Story Format (CSF) loader used it to modify a story's display name independent of the story's `name`/`id` (which were coupled).
In 5.3, the CSF loader decouples the story's `name`/`id`, which means that `displayName` is no longer necessary. Unfortunately, this is a breaking change for any code that uses the story `name` field. Storyshots relies on story `name`, and the appropriate migration is to simply update your snapshots. Apologies for the inconvenience!
### Unified docs preset
Addon-docs configuration gets simpler in 5.3. In 5.2, each framework had its own preset, e.g. `@storybook/addon-docs/react/preset`. Starting in 5.3, everybody should use `@storybook/addon-docs/preset`.
### Simplified hierarchy separators
We've deprecated the ability to specify the hierarchy separators (how you control the grouping of story kinds in the sidebar). From Storybook 6.0 we will have a single separator `/`, which cannot be configured.
If you are currently using using custom separators, we encourage you to migrate to using `/` as the sole separator. If you are using `|` or `.` as a separator currently, (we will soon provide) a codemod that can be used to rename all your components.
If you were using `|` and wish to keep the "root" behaviour, use the `showRoots: true` option to re-enable roots:
```js
addParameters({ options: { showRoots: true } });
```
NOTE: it is no longer possible to have some stories with roots and others without. If you want to keep the old behaviour, simply add a root called "Others" to all your previously unrooted stories.
## From version 5.1.x to 5.2.x
### Source-loader

View File

@ -56,7 +56,6 @@ import { withA11y } from '@storybook/addon-a11y';
addDecorator(withA11y)
addParameters({
a11y: {
// ... axe options
element: '#root', // optional selector which element to inspect
config: {}, // axe-core configurationOptions (https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#parameters-1)
options: {} // axe-core optionsParameter (https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#options-parameter)

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-a11y",
"version": "5.3.0-alpha.41",
"version": "5.3.0-beta.2",
"description": "a11y addon for storybook",
"keywords": [
"a11y",
@ -33,12 +33,12 @@
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "5.3.0-alpha.41",
"@storybook/api": "5.3.0-alpha.41",
"@storybook/client-logger": "5.3.0-alpha.41",
"@storybook/components": "5.3.0-alpha.41",
"@storybook/core-events": "5.3.0-alpha.41",
"@storybook/theming": "5.3.0-alpha.41",
"@storybook/addons": "5.3.0-beta.2",
"@storybook/api": "5.3.0-beta.2",
"@storybook/client-logger": "5.3.0-beta.2",
"@storybook/components": "5.3.0-beta.2",
"@storybook/core-events": "5.3.0-beta.2",
"@storybook/theming": "5.3.0-beta.2",
"axe-core": "^3.3.2",
"core-js": "^3.0.1",
"global": "^4.3.2",

View File

@ -63,7 +63,7 @@ function ThemedA11YPanel(props) {
}
describe('A11YPanel', () => {
it('should register STORY_RENDERED and RESULT updater on mount', () => {
it('should register STORY_RENDERED, RESULT and ERROR updater on mount', () => {
// given
const api = createApi();
expect(api.on).not.toHaveBeenCalled();
@ -72,9 +72,10 @@ describe('A11YPanel', () => {
mount(<ThemedA11YPanel api={api} />);
// then
expect(api.on.mock.calls.length).toBe(2);
expect(api.on.mock.calls.length).toBe(3);
expect(api.on.mock.calls[0][0]).toBe(STORY_RENDERED);
expect(api.on.mock.calls[1][0]).toBe(EVENTS.RESULT);
expect(api.on.mock.calls[2][0]).toBe(EVENTS.ERROR);
});
it('should request a run on tab activation', () => {
@ -93,7 +94,7 @@ describe('A11YPanel', () => {
expect(wrapper.find(ScrollArea).length).toBe(0);
});
it('should deregister STORY_RENDERED and RESULT updater on unmount', () => {
it('should deregister STORY_RENDERED, RESULT and ERROR updater on unmount', () => {
// given
const api = createApi();
const wrapper = mount(<ThemedA11YPanel api={api} />);
@ -103,9 +104,10 @@ describe('A11YPanel', () => {
wrapper.unmount();
// then
expect(api.off.mock.calls.length).toBe(2);
expect(api.off.mock.calls.length).toBe(3);
expect(api.off.mock.calls[0][0]).toBe(STORY_RENDERED);
expect(api.off.mock.calls[1][0]).toBe(EVENTS.RESULT);
expect(api.off.mock.calls[2][0]).toBe(EVENTS.ERROR);
});
it('should update run result', () => {

View File

@ -46,26 +46,35 @@ const Incomplete = styled.span<{}>(({ theme }) => ({
color: theme.color.warning,
}));
const centeredStyle = {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
};
const Loader = styled(({ className }) => (
<div className={className}>
<Icon inline icon="sync" status="running" /> Please wait while the accessibility scan is running
...
</div>
))({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
});
))(centeredStyle);
Loader.displayName = 'Loader';
interface A11YPanelState {
status: string;
interface A11YPanelNormalState {
status: 'ready' | 'ran' | 'running';
passes: Result[];
violations: Result[];
incomplete: Result[];
}
interface A11YPanelErrorState {
status: 'error';
error: unknown;
}
type A11YPanelState = A11YPanelNormalState | A11YPanelErrorState;
interface A11YPanelProps {
active: boolean;
api: API;
@ -84,6 +93,7 @@ export class A11YPanel extends Component<A11YPanelProps, A11YPanelState> {
api.on(STORY_RENDERED, this.request);
api.on(EVENTS.RESULT, this.onUpdate);
api.on(EVENTS.ERROR, this.onError);
}
componentDidUpdate(prevProps: A11YPanelProps) {
@ -101,6 +111,7 @@ export class A11YPanel extends Component<A11YPanelProps, A11YPanelState> {
const { api } = this.props;
api.off(STORY_RENDERED, this.request);
api.off(EVENTS.RESULT, this.onUpdate);
api.off(EVENTS.ERROR, this.onError);
}
onUpdate = ({ passes, violations, incomplete }: AxeResults) => {
@ -124,6 +135,13 @@ export class A11YPanel extends Component<A11YPanelProps, A11YPanelState> {
);
};
onError = (error: unknown) => {
this.setState({
status: 'error',
error,
});
};
request = () => {
const { api, active } = this.props;
@ -142,8 +160,22 @@ export class A11YPanel extends Component<A11YPanelProps, A11YPanelState> {
};
render() {
const { passes, violations, incomplete, status } = this.state;
const { active } = this.props;
if (!active) return null;
// eslint-disable-next-line react/destructuring-assignment
if (this.state.status === 'error') {
const { error } = this.state;
return (
<div style={centeredStyle}>
The accessibility scan encountered an error.
<br />
{error}
</div>
);
}
const { passes, violations, incomplete, status } = this.state;
let actionTitle;
if (status === 'ready') {
@ -162,7 +194,7 @@ export class A11YPanel extends Component<A11YPanelProps, A11YPanelState> {
);
}
return active ? (
return (
<Fragment>
<Provider store={store}>
{status === 'running' ? (
@ -218,6 +250,6 @@ export class A11YPanel extends Component<A11YPanelProps, A11YPanelState> {
/>
</Provider>
</Fragment>
) : null;
);
}
}

View File

@ -185,7 +185,4 @@ class HighlightToggle extends Component<ToggleProps> {
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(HighlightToggle);
export default connect(mapStateToProps, mapDispatchToProps)(HighlightToggle);

View File

@ -84,6 +84,7 @@ exports[`HighlightToggle component should match snapshot 1`] = `
"map": "/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9hbmltYXRpb24udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBd0NxQiIsImZpbGUiOiIuLi9zcmMvYW5pbWF0aW9uLnRzIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgY3NzLCBrZXlmcmFtZXMgfSBmcm9tICdAZW1vdGlvbi9jb3JlJztcblxuZXhwb3J0IGNvbnN0IGVhc2luZyA9IHtcbiAgcnViYmVyOiAnY3ViaWMtYmV6aWVyKDAuMTc1LCAwLjg4NSwgMC4zMzUsIDEuMDUpJyxcbn07XG5cbmNvbnN0IHJvdGF0ZTM2MCA9IGtleWZyYW1lc2Bcblx0ZnJvbSB7XG5cdFx0dHJhbnNmb3JtOiByb3RhdGUoMGRlZyk7XG5cdH1cblx0dG8ge1xuXHRcdHRyYW5zZm9ybTogcm90YXRlKDM2MGRlZyk7XG5cdH1cbmA7XG5cbmNvbnN0IGdsb3cgPSBrZXlmcmFtZXNgXG4gIDAlLCAxMDAlIHsgb3BhY2l0eTogMTsgfVxuICA1MCUgeyBvcGFjaXR5OiAuNDsgfVxuYDtcblxuY29uc3QgZmxvYXQgPSBrZXlmcmFtZXNgXG4gIDAlIHsgdHJhbnNmb3JtOiB0cmFuc2xhdGVZKDFweCk7IH1cbiAgMjUlIHsgdHJhbnNmb3JtOiB0cmFuc2xhdGVZKDBweCk7IH1cbiAgNTAlIHsgdHJhbnNmb3JtOiB0cmFuc2xhdGVZKC0zcHgpOyB9XG4gIDEwMCUgeyB0cmFuc2Zvcm06IHRyYW5zbGF0ZVkoMXB4KTsgfVxuYDtcblxuY29uc3QgamlnZ2xlID0ga2V5ZnJhbWVzYFxuICAwJSwgMTAwJSB7IHRyYW5zZm9ybTp0cmFuc2xhdGUzZCgwLDAsMCk7IH1cbiAgMTIuNSUsIDYyLjUlIHsgdHJhbnNmb3JtOnRyYW5zbGF0ZTNkKC00cHgsMCwwKTsgfVxuICAzNy41JSwgODcuNSUgeyAgdHJhbnNmb3JtOiB0cmFuc2xhdGUzZCg0cHgsMCwwKTsgIH1cbmA7XG5cbmNvbnN0IGlubGluZUdsb3cgPSBjc3NgXG4gIGFuaW1hdGlvbjogJHtnbG93fSAxLjVzIGVhc2UtaW4tb3V0IGluZmluaXRlO1xuICBjb2xvcjogdHJhbnNwYXJlbnQ7XG4gIGN1cnNvcjogcHJvZ3Jlc3M7XG5gO1xuXG4vLyBob3ZlciAmIGFjdGl2ZSBzdGF0ZSBmb3IgbGlua3MgYW5kIGJ1dHRvbnNcbmNvbnN0IGhvdmVyYWJsZSA9IGNzc2BcbiAgdHJhbnNpdGlvbjogYWxsIDE1MG1zIGVhc2Utb3V0O1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZTNkKDAsIDAsIDApO1xuXG4gICY6aG92ZXIge1xuICAgIHRyYW5zZm9ybTogdHJhbnNsYXRlM2QoMCwgLTJweCwgMCk7XG4gIH1cblxuICAmOmFjdGl2ZSB7XG4gICAgdHJhbnNmb3JtOiB0cmFuc2xhdGUzZCgwLCAwLCAwKTtcbiAgfVxuYDtcblxuZXhwb3J0IGNvbnN0IGFuaW1hdGlvbiA9IHtcbiAgcm90YXRlMzYwLFxuICBnbG93LFxuICBmbG9hdCxcbiAgamlnZ2xlLFxuICBpbmxpbmVHbG93LFxuICBob3ZlcmFibGUsXG59O1xuIl19 */",
"name": "1o7rzh8-hoverable",
"styles": "transition:all 150ms ease-out;transform:translate3d(0,0,0);&:hover{transform:translate3d(0,-2px,0);}&:active{transform:translate3d(0,0,0);};label:hoverable;",
"toString": [Function],
},
"inlineGlow": Object {
"map": "/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9hbmltYXRpb24udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBaUNzQiIsImZpbGUiOiIuLi9zcmMvYW5pbWF0aW9uLnRzIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgY3NzLCBrZXlmcmFtZXMgfSBmcm9tICdAZW1vdGlvbi9jb3JlJztcblxuZXhwb3J0IGNvbnN0IGVhc2luZyA9IHtcbiAgcnViYmVyOiAnY3ViaWMtYmV6aWVyKDAuMTc1LCAwLjg4NSwgMC4zMzUsIDEuMDUpJyxcbn07XG5cbmNvbnN0IHJvdGF0ZTM2MCA9IGtleWZyYW1lc2Bcblx0ZnJvbSB7XG5cdFx0dHJhbnNmb3JtOiByb3RhdGUoMGRlZyk7XG5cdH1cblx0dG8ge1xuXHRcdHRyYW5zZm9ybTogcm90YXRlKDM2MGRlZyk7XG5cdH1cbmA7XG5cbmNvbnN0IGdsb3cgPSBrZXlmcmFtZXNgXG4gIDAlLCAxMDAlIHsgb3BhY2l0eTogMTsgfVxuICA1MCUgeyBvcGFjaXR5OiAuNDsgfVxuYDtcblxuY29uc3QgZmxvYXQgPSBrZXlmcmFtZXNgXG4gIDAlIHsgdHJhbnNmb3JtOiB0cmFuc2xhdGVZKDFweCk7IH1cbiAgMjUlIHsgdHJhbnNmb3JtOiB0cmFuc2xhdGVZKDBweCk7IH1cbiAgNTAlIHsgdHJhbnNmb3JtOiB0cmFuc2xhdGVZKC0zcHgpOyB9XG4gIDEwMCUgeyB0cmFuc2Zvcm06IHRyYW5zbGF0ZVkoMXB4KTsgfVxuYDtcblxuY29uc3QgamlnZ2xlID0ga2V5ZnJhbWVzYFxuICAwJSwgMTAwJSB7IHRyYW5zZm9ybTp0cmFuc2xhdGUzZCgwLDAsMCk7IH1cbiAgMTIuNSUsIDYyLjUlIHsgdHJhbnNmb3JtOnRyYW5zbGF0ZTNkKC00cHgsMCwwKTsgfVxuICAzNy41JSwgODcuNSUgeyAgdHJhbnNmb3JtOiB0cmFuc2xhdGUzZCg0cHgsMCwwKTsgIH1cbmA7XG5cbmNvbnN0IGlubGluZUdsb3cgPSBjc3NgXG4gIGFuaW1hdGlvbjogJHtnbG93fSAxLjVzIGVhc2UtaW4tb3V0IGluZmluaXRlO1xuICBjb2xvcjogdHJhbnNwYXJlbnQ7XG4gIGN1cnNvcjogcHJvZ3Jlc3M7XG5gO1xuXG4vLyBob3ZlciAmIGFjdGl2ZSBzdGF0ZSBmb3IgbGlua3MgYW5kIGJ1dHRvbnNcbmNvbnN0IGhvdmVyYWJsZSA9IGNzc2BcbiAgdHJhbnNpdGlvbjogYWxsIDE1MG1zIGVhc2Utb3V0O1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZTNkKDAsIDAsIDApO1xuXG4gICY6aG92ZXIge1xuICAgIHRyYW5zZm9ybTogdHJhbnNsYXRlM2QoMCwgLTJweCwgMCk7XG4gIH1cblxuICAmOmFjdGl2ZSB7XG4gICAgdHJhbnNmb3JtOiB0cmFuc2xhdGUzZCgwLCAwLCAwKTtcbiAgfVxuYDtcblxuZXhwb3J0IGNvbnN0IGFuaW1hdGlvbiA9IHtcbiAgcm90YXRlMzYwLFxuICBnbG93LFxuICBmbG9hdCxcbiAgamlnZ2xlLFxuICBpbmxpbmVHbG93LFxuICBob3ZlcmFibGUsXG59O1xuIl19 */",

View File

@ -177,6 +177,10 @@ exports[`A11YPanel should render report 1`] = `
"storybook/a11y/result",
[Function],
],
Array [
"storybook/a11y/error",
[Function],
],
],
"results": Array [
Object {
@ -187,6 +191,10 @@ exports[`A11YPanel should render report 1`] = `
"type": "return",
"value": undefined,
},
Object {
"type": "return",
"value": undefined,
},
],
},
}

View File

@ -6,5 +6,6 @@ export const ADD_ELEMENT = 'ADD_ELEMENT';
export const CLEAR_ELEMENTS = 'CLEAR_ELEMENTS';
const RESULT = `${ADDON_ID}/result`;
const REQUEST = `${ADDON_ID}/request`;
const ERROR = `${ADDON_ID}/error`;
export const EVENTS = { RESULT, REQUEST };
export const EVENTS = { RESULT, REQUEST, ERROR };

View File

@ -39,7 +39,8 @@ const run = (element: ElementContext, config: Spec, options: RunOptions) => {
restoreScroll: true,
} as RunOptions) // cast to RunOptions is necessary because axe types are not up to date
)
.then(report);
.then(report)
.catch(error => addons.getChannel().emit(EVENTS.ERROR, String(error)));
});
};
@ -47,12 +48,20 @@ if (module && module.hot && module.hot.decline) {
module.hot.decline();
}
let storedDefaultSetup: Setup | null = null;
export const withA11y = makeDecorator({
name: 'withA11Y',
parameterName: PARAM_KEY,
wrapper: (getStory, context, { parameters }) => {
if (parameters) {
setup = parameters as Setup;
if (storedDefaultSetup === null) {
storedDefaultSetup = { ...setup };
}
Object.assign(setup, parameters as Setup);
} else if (storedDefaultSetup !== null) {
Object.assign(setup, storedDefaultSetup);
storedDefaultSetup = null;
}
addons.getChannel().on(EVENTS.REQUEST, () => run(setup.element, setup.config, setup.options));

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-actions",
"version": "5.3.0-alpha.41",
"version": "5.3.0-beta.2",
"description": "Action Logger addon for storybook",
"keywords": [
"storybook"
@ -28,12 +28,12 @@
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "5.3.0-alpha.41",
"@storybook/api": "5.3.0-alpha.41",
"@storybook/client-api": "5.3.0-alpha.41",
"@storybook/components": "5.3.0-alpha.41",
"@storybook/core-events": "5.3.0-alpha.41",
"@storybook/theming": "5.3.0-alpha.41",
"@storybook/addons": "5.3.0-beta.2",
"@storybook/api": "5.3.0-beta.2",
"@storybook/client-api": "5.3.0-beta.2",
"@storybook/components": "5.3.0-beta.2",
"@storybook/core-events": "5.3.0-beta.2",
"@storybook/theming": "5.3.0-beta.2",
"core-js": "^3.0.1",
"fast-deep-equal": "^2.0.1",
"global": "^4.3.2",

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-backgrounds",
"version": "5.3.0-alpha.41",
"version": "5.3.0-beta.2",
"description": "A storybook addon to show different backgrounds for your preview",
"keywords": [
"addon",
@ -32,12 +32,12 @@
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "5.3.0-alpha.41",
"@storybook/api": "5.3.0-alpha.41",
"@storybook/client-logger": "5.3.0-alpha.41",
"@storybook/components": "5.3.0-alpha.41",
"@storybook/core-events": "5.3.0-alpha.41",
"@storybook/theming": "5.3.0-alpha.41",
"@storybook/addons": "5.3.0-beta.2",
"@storybook/api": "5.3.0-beta.2",
"@storybook/client-logger": "5.3.0-beta.2",
"@storybook/components": "5.3.0-beta.2",
"@storybook/core-events": "5.3.0-beta.2",
"@storybook/theming": "5.3.0-beta.2",
"core-js": "^3.0.1",
"memoizerific": "^1.11.3",
"react": "^16.8.3",

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-centered",
"version": "5.3.0-alpha.41",
"version": "5.3.0-beta.2",
"description": "Storybook decorator to center components",
"keywords": [
"addon",
@ -29,7 +29,7 @@
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "5.3.0-alpha.41",
"@storybook/addons": "5.3.0-beta.2",
"core-js": "^3.0.1",
"global": "^4.3.2",
"util-deprecate": "^1.0.2"

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-contexts",
"version": "5.3.0-alpha.41",
"version": "5.3.0-beta.2",
"description": "Storybook Addon Contexts",
"keywords": [
"preact",
@ -27,10 +27,10 @@
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "5.3.0-alpha.41",
"@storybook/api": "5.3.0-alpha.41",
"@storybook/components": "5.3.0-alpha.41",
"@storybook/core-events": "5.3.0-alpha.41",
"@storybook/addons": "5.3.0-beta.2",
"@storybook/api": "5.3.0-beta.2",
"@storybook/components": "5.3.0-beta.2",
"@storybook/core-events": "5.3.0-beta.2",
"core-js": "^3.0.1",
"global": "^4.3.2",
"qs": "^6.6.0"

View File

@ -27,7 +27,10 @@ describe('Tests on addon-contexts component: ToolBar', () => {
icon: 'box' as const,
nodeId: 'Some Context B',
options: { cancelable: true, deep: false, disable: false },
params: [{ name: 'Some Param X', props: {} }, { name: 'Some Param Y', props: {} }],
params: [
{ name: 'Some Param X', props: {} },
{ name: 'Some Param Y', props: {} },
],
title: 'Some Context B',
},
];

View File

@ -9,7 +9,10 @@ describe('Tests on addon-contexts component: ToolBarControl', () => {
icon: 'box' as const,
nodeId: 'Some Context',
options: { cancelable: true, deep: false, disable: false },
params: [{ name: 'A', props: {} }, { name: 'B', props: {} }],
params: [
{ name: 'A', props: {} },
{ name: 'B', props: {} },
],
title: 'Some Context',
selected: '',
setSelected: jest.fn,

View File

@ -22,7 +22,10 @@ describe('Test on the merging result of a pair of settings', () => {
icon: 'box' as const,
title: 'Some Context',
components: ['div'],
params: [{ name: 'T1', props: {} }, { name: 'T2', props: {} }],
params: [
{ name: 'T1', props: {} },
{ name: 'T2', props: {} },
],
options: {
cancelable: true,
disable: true,
@ -32,7 +35,10 @@ describe('Test on the merging result of a pair of settings', () => {
icon: 'box' as const,
title: 'Some Context',
components: ['span'],
params: [{ name: 'S1', props: {} }, { name: 'S2', props: {} }],
params: [
{ name: 'S1', props: {} },
{ name: 'S2', props: {} },
],
options: {
deep: true,
disable: false,
@ -106,7 +112,10 @@ describe('Test on reconciliation of settings', () => {
icon: 'box',
nodeId: 'Some Context',
options: { cancelable: false, deep: false, disable: false },
params: [{ name: 'T1', props: {} }, { name: 'S2', props: {}, default: true }],
params: [
{ name: 'T1', props: {} },
{ name: 'S2', props: {}, default: true },
],
title: 'Some Context',
},
{

View File

@ -2,7 +2,10 @@ import { _getPropsByParamName, getPropsMap } from './getPropsMap';
import { OPT_OUT } from '../../shared/constants';
describe('Test on behaviors from collecting the propsMap', () => {
const someParams = [{ name: 'A', props: {} }, { name: 'B', props: {} }];
const someParams = [
{ name: 'A', props: {} },
{ name: 'B', props: {} },
];
it('should return "null" when params in 0 length', () => {
const result = _getPropsByParamName([]);
@ -44,7 +47,10 @@ describe('Test on the integrity of the method to get the propMaps', () => {
icon: 'box' as const,
nodeId: 'Some Context',
options: { cancelable: false, deep: false, disable: false },
params: [{ name: 'A1', props: { a: 1 } }, { name: 'A2', props: { a: 2 }, default: true }],
params: [
{ name: 'A1', props: { a: 1 } },
{ name: 'A2', props: { a: 2 }, default: true },
],
title: 'Some Context',
},
{

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-cssresources",
"version": "5.3.0-alpha.41",
"version": "5.3.0-beta.2",
"description": "A storybook addon to switch between css resources at runtime for your story",
"keywords": [
"addon",
@ -32,10 +32,10 @@
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "5.3.0-alpha.41",
"@storybook/api": "5.3.0-alpha.41",
"@storybook/components": "5.3.0-alpha.41",
"@storybook/core-events": "5.3.0-alpha.41",
"@storybook/addons": "5.3.0-beta.2",
"@storybook/api": "5.3.0-beta.2",
"@storybook/components": "5.3.0-beta.2",
"@storybook/core-events": "5.3.0-beta.2",
"core-js": "^3.0.1",
"global": "^4.3.2",
"react": "^16.8.3"

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-design-assets",
"version": "5.3.0-alpha.41",
"version": "5.3.0-beta.2",
"description": "Design asset preview for storybook",
"keywords": [
"addon",
@ -34,12 +34,12 @@
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "5.3.0-alpha.41",
"@storybook/api": "5.3.0-alpha.41",
"@storybook/client-logger": "5.3.0-alpha.41",
"@storybook/components": "5.3.0-alpha.41",
"@storybook/core-events": "5.3.0-alpha.41",
"@storybook/theming": "5.3.0-alpha.41",
"@storybook/addons": "5.3.0-beta.2",
"@storybook/api": "5.3.0-beta.2",
"@storybook/client-logger": "5.3.0-beta.2",
"@storybook/components": "5.3.0-beta.2",
"@storybook/core-events": "5.3.0-beta.2",
"@storybook/theming": "5.3.0-beta.2",
"core-js": "^3.0.1",
"global": "^4.3.2",
"react": "^16.8.3",

View File

@ -47,7 +47,7 @@ Here's an example file:
import { Meta, Story, Preview } from '@storybook/addon-docs/blocks';
import { Checkbox } from './Checkbox';
<Meta title="MDX|Checkbox" component={Checkbox} />
<Meta title="MDX/Checkbox" component={Checkbox} />
# Checkbox
@ -77,16 +77,16 @@ For more information on `MDX`, see the [`MDX` reference](./docs/mdx.md).
Storybook Docs supports all view layers that Storybook supports except for React Native (currently). There are some framework-specific features as well, such as props tables and inline story rendering. This chart captures the current state of support:
| | React | Vue | Angular | HTML | [Web Components](./web-components) | Svelte | Polymer | Marko | Mithril | Riot | Ember | Preact |
| ----------------- | :---: | :---: | :-----: | :---: | :--------------------------------: | :----: | :-----: | :---: | :-----: | :---: | :---: | :----: |
| MDX stories | + | + | + | + | + | + | + | + | + | + | + | + |
| CSF stories | + | + | + | + | + | + | + | + | + | + | + | + |
| StoriesOf stories | + | + | + | + | + | + | + | + | + | + | + | + |
| Source | + | + | + | + | + | + | + | + | + | + | + | + |
| Notes / Info | + | + | + | + | + | + | + | + | + | + | + | + |
| Props table | + | + | # | | + | | | | | | | |
| Description | + | + | # | | + | | | | | | | |
| Inline stories | + | + | | | + | | | | | | | |
| | React | Vue | Angular | HTML | [Web Components](./web-components) | Svelte | Polymer | Marko | Mithril | Riot | Ember | Preact |
| ----------------- | :---: | :-: | :-----: | :--: | :--------------------------------: | :----: | :-----: | :---: | :-----: | :--: | :---: | :----: |
| MDX stories | + | + | + | + | + | + | + | + | + | + | + | + |
| CSF stories | + | + | + | + | + | + | + | + | + | + | + | + |
| StoriesOf stories | + | + | + | + | + | + | + | + | + | + | + | + |
| Source | + | + | + | + | + | + | + | + | + | + | + | + |
| Notes / Info | + | + | + | + | + | + | + | + | + | + | + | + |
| Props table | + | + | # | | + | | | | | | | |
| Description | + | + | # | | + | | | | | | | |
| Inline stories | + | + | | | + | | | | | | | |
**Note:** `#` = WIP support
@ -109,11 +109,9 @@ yarn add -D react react-is babel-loader
Then add the following to your `.storybook/presets.js` exports:
```js
module.exports = ['@storybook/addon-docs/react/preset'];
module.exports = ['@storybook/addon-docs/preset'];
```
If you're not using `react`, replace it with your framework of choice corresponding to the Storybook package name, e.g. `angular` for `@storybook/angular` etc.
**Configure.** If you're migrating from an earlier version of Storybook and want to use `MDX`, you need to upgrade your Storybook config:
```js

View File

@ -25,7 +25,7 @@ yarn add -D @storybook/addon-docs@next
Then add the following to your `.storybook/presets.js` exports:
```js
module.exports = ['@storybook/addon-docs/angular/preset'];
module.exports = ['@storybook/addon-docs/preset'];
```
## DocsPage

View File

@ -1 +0,0 @@
module.exports = require('../dist/frameworks/angular/config');

View File

@ -25,7 +25,7 @@ Let's get started with an example that combines markdown with a single story:
import { Meta, Story, Preview } from '@storybook/addon-docs/blocks';
import { Checkbox } from './Checkbox';
<Meta title="MDX|Checkbox" component={Checkbox} />
<Meta title="MDX/Checkbox" component={Checkbox} />
# Checkbox
@ -64,7 +64,7 @@ For example, here's the story from `Checkbox` example above, rewritten in CSF:
```js
import React from 'react';
import { Checkbox } from './Checkbox';
export default { title: "MDX|Checkbox" component: Checkbox };
export default { title: "MDX/Checkbox" component: Checkbox };
export const allCheckboxes = () => (
<form>
<Checkbox id="Unchecked" label="Unchecked" />
@ -86,7 +86,7 @@ import { Meta, Story, Preview } from '@storybook/addon-docs/blocks';
import { Badge } from './Badge';
import { Icon } from './Icon';
<Meta title="MDX|Badge" component={Badge} />
<Meta title="MDX/Badge" component={Badge} />
# Badge

View File

@ -66,7 +66,7 @@ import { SomeComponent } from 'path/to/SomeComponent';
I can define a story with the function imported from CSF:
<Story name="basic">{stories.basic}</Story>
<Story name="basic">{stories.basic()}</Story>
And I can also embed arbitrary markdown & JSX in this file.

View File

@ -1,6 +1,6 @@
{
"name": "@storybook/addon-docs",
"version": "5.3.0-alpha.41",
"version": "5.3.0-beta.2",
"description": "Superior documentation for your components",
"keywords": [
"addon",
@ -23,6 +23,7 @@
"angular/**/*",
"common/**/*",
"html/**/*",
"postinstall/**/*",
"react/**/*",
"vue/**/*",
"web-components/**/*",
@ -44,20 +45,28 @@
"@mdx-js/loader": "^1.1.0",
"@mdx-js/mdx": "^1.1.0",
"@mdx-js/react": "^1.0.27",
"@storybook/addons": "5.3.0-alpha.41",
"@storybook/api": "5.3.0-alpha.41",
"@storybook/components": "5.3.0-alpha.41",
"@storybook/router": "5.3.0-alpha.41",
"@storybook/source-loader": "5.3.0-alpha.41",
"@storybook/theming": "5.3.0-alpha.41",
"@storybook/addons": "5.3.0-beta.2",
"@storybook/api": "5.3.0-beta.2",
"@storybook/components": "5.3.0-beta.2",
"@storybook/postinstall": "5.3.0-beta.2",
"@storybook/router": "5.3.0-beta.2",
"@storybook/source-loader": "5.3.0-beta.2",
"@storybook/theming": "5.3.0-beta.2",
"acorn": "^7.1.0",
"acorn-jsx": "^5.1.0",
"acorn-walk": "^7.0.0",
"core-js": "^3.0.1",
"doctrine": "^3.0.0",
"escodegen": "^1.12.0",
"global": "^4.3.2",
"html-tags": "^3.1.0",
"js-string-escape": "^1.0.1",
"lodash": "^4.17.15",
"prop-types": "^15.7.2",
"storybook-addon-vue-info": "^1.2.1",
"ts-dedent": "^1.1.0"
"ts-dedent": "^1.1.0",
"util-deprecate": "^1.0.2",
"vue-docgen-api": "^3.26.0",
"vue-docgen-loader": "^1.0.1"
},
"devDependencies": {
"@types/doctrine": "^0.0.3",
@ -65,7 +74,8 @@
"@types/jest": "^24.0.11",
"@types/prop-types": "^15.5.9",
"@types/util-deprecate": "^1.0.0",
"@types/webpack-env": "^1.14.0"
"@types/webpack-env": "^1.14.0",
"jest-specific-snapshot": "^2.0.0"
},
"peerDependencies": {
"babel-loader": "^8.0.0",

View File

@ -0,0 +1,39 @@
import fs from 'fs';
import { presetsAddPreset, getFrameworks } from '@storybook/postinstall';
// eslint-disable-next-line import/no-extraneous-dependencies
import { logger } from '@storybook/node-logger';
export default function transformer(file, api) {
const packageJson = JSON.parse(fs.readFileSync('./package.json'));
const frameworks = getFrameworks(packageJson);
let err = null;
let framework = null;
let presetOptions = null;
if (frameworks.length !== 1) {
err = `${frameworks.length === 0 ? 'No' : 'Multiple'} frameworks found: ${frameworks}`;
logger.error(`${err}, please configure '@storybook/addon-docs' manually.`);
return file.source;
}
// eslint-disable-next-line prefer-destructuring
framework = frameworks[0];
const { dependencies, devDependencies } = packageJson;
if (
framework === 'react' &&
((dependencies && dependencies['react-scripts']) ||
(devDependencies && devDependencies['react-scripts']))
) {
presetOptions = {
configureJSX: true,
};
}
const j = api.jscodeshift;
const root = j(file.source);
presetsAddPreset(`@storybook/addon-docs/preset`, presetOptions, { root, api });
return root.toSource({ quote: 'single' });
}

1
addons/docs/preset.js Normal file
View File

@ -0,0 +1 @@
module.exports = require('./dist/preset');

View File

@ -1,8 +1,8 @@
import React, { FunctionComponent } from 'react';
import React, { FunctionComponent, useContext } from 'react';
import { Description, DescriptionProps as PureDescriptionProps } from '@storybook/components';
import { DocsContext, DocsContextProps } from './DocsContext';
import { Component, CURRENT_SELECTION } from './shared';
import { str } from '../lib/docgenUtils';
import { Component, CURRENT_SELECTION, DescriptionSlot } from './shared';
import { str } from '../lib/docgen/utils';
export enum DescriptionType {
INFO = 'info',
@ -16,24 +16,26 @@ type Notes = string | any;
type Info = string | any;
interface DescriptionProps {
slot?: DescriptionSlot;
of?: '.' | Component;
type?: DescriptionType;
markdown?: string;
children?: string;
}
export const getNotes = (notes?: Notes) =>
const getNotes = (notes?: Notes) =>
notes && (typeof notes === 'string' ? notes : str(notes.markdown) || str(notes.text));
export const getInfo = (info?: Info) => info && (typeof info === 'string' ? info : str(info.text));
const getInfo = (info?: Info) => info && (typeof info === 'string' ? info : str(info.text));
const noDescription = (component?: Component): string | null => null;
export const getDescriptionProps = (
{ of, type, markdown }: DescriptionProps,
{ of, type, markdown, children }: DescriptionProps,
{ parameters }: DocsContextProps
): PureDescriptionProps => {
if (markdown) {
return { markdown };
if (children || markdown) {
return { markdown: children || markdown };
}
const { component, notes, info, docs } = parameters;
const { extractComponentDescription = noDescription } = docs || {};
@ -59,13 +61,19 @@ ${extractComponentDescription(target) || ''}
}
};
const DescriptionContainer: FunctionComponent<DescriptionProps> = props => (
<DocsContext.Consumer>
{context => {
const { markdown } = getDescriptionProps(props, context);
return markdown && <Description markdown={markdown} />;
}}
</DocsContext.Consumer>
);
const DescriptionContainer: FunctionComponent<DescriptionProps> = props => {
const context = useContext(DocsContext);
const { slot } = props;
let { markdown } = getDescriptionProps(props, context);
if (slot) {
markdown = slot(markdown, context);
}
return markdown ? <Description markdown={markdown} /> : null;
};
// since we are in the docs blocks, assume default description if for primary component story
DescriptionContainer.defaultProps = {
of: '.',
};
export { DescriptionContainer as Description };

View File

@ -0,0 +1,18 @@
import { defaultTitleSlot } from './Title';
describe('defaultTitleSlot', () => {
it('showRoots', () => {
const parameters = {
options: { showRoots: true },
};
expect(defaultTitleSlot({ selectedKind: 'a/b/c', parameters })).toBe('c');
expect(defaultTitleSlot({ selectedKind: 'a|b', parameters })).toBe('a|b');
expect(defaultTitleSlot({ selectedKind: 'a/b/c.d', parameters })).toBe('c.d');
});
it('no showRoots', () => {
const parameters = {};
expect(defaultTitleSlot({ selectedKind: 'a/b/c', parameters })).toBe('c');
expect(defaultTitleSlot({ selectedKind: 'a|b', parameters })).toBe('b');
expect(defaultTitleSlot({ selectedKind: 'a/b/c.d', parameters })).toBe('d');
});
});

View File

@ -1,140 +1,26 @@
import React, { FunctionComponent } from 'react';
import { parseKind } from '@storybook/router';
import { DocsPage as PureDocsPage, PropsTable, PropsTableProps } from '@storybook/components';
import { H2, H3 } from '@storybook/components/html';
import { DocsContext } from './DocsContext';
import { DocsPageProps } from './shared';
import { Title } from './Title';
import { Subtitle } from './Subtitle';
import { Description } from './Description';
import { Story } from './Story';
import { Preview } from './Preview';
import { Anchor } from './Anchor';
import { getPropsTableProps } from './Props';
export interface SlotContext {
id?: string;
selectedKind?: string;
selectedStory?: string;
parameters?: any;
storyStore?: any;
}
export type StringSlot = (context: SlotContext) => string | void;
export type PropsSlot = (context: SlotContext) => PropsTableProps | void;
export type StorySlot = (stories: StoryData[], context: SlotContext) => DocsStoryProps | void;
export type StoriesSlot = (stories: StoryData[], context: SlotContext) => DocsStoryProps[] | void;
export interface DocsPageProps {
titleSlot: StringSlot;
subtitleSlot: StringSlot;
descriptionSlot: StringSlot;
primarySlot: StorySlot;
propsSlot: PropsSlot;
storiesSlot: StoriesSlot;
}
interface DocsStoryProps {
id: string;
name: string;
expanded?: boolean;
withToolbar?: boolean;
parameters?: any;
}
interface StoryData {
id: string;
kind: string;
name: string;
parameters?: any;
}
const defaultTitleSlot: StringSlot = ({ selectedKind, parameters }) => {
const {
hierarchyRootSeparator: rootSeparator,
hierarchySeparator: groupSeparator,
} = (parameters && parameters.options) || {
hierarchyRootSeparator: '|',
hierarchySeparator: /\/|\./,
};
const { groups } = parseKind(selectedKind, { rootSeparator, groupSeparator });
return (groups && groups[groups.length - 1]) || selectedKind;
};
const defaultSubtitleSlot: StringSlot = ({ parameters }) =>
parameters && parameters.componentSubtitle;
const defaultPropsSlot: PropsSlot = context => getPropsTableProps({ of: '.' }, context);
const defaultDescriptionSlot: StringSlot = ({ parameters }) => {
const { component, docs } = parameters;
if (!component) {
return null;
}
const { extractComponentDescription } = docs || {};
return extractComponentDescription && extractComponentDescription(component, parameters);
};
const defaultPrimarySlot: StorySlot = stories => stories && stories[0];
const defaultStoriesSlot: StoriesSlot = stories => {
if (stories && stories.length > 1) {
const [first, ...rest] = stories;
return rest;
}
return null;
};
const StoriesHeading = H2;
const StoryHeading = H3;
const DocsStory: FunctionComponent<DocsStoryProps> = ({
id,
name,
expanded = true,
withToolbar = false,
parameters,
}) => (
<Anchor storyId={id}>
{expanded && <StoryHeading>{(parameters && parameters.displayName) || name}</StoryHeading>}
{expanded && parameters && parameters.docs && parameters.docs.storyDescription && (
<Description markdown={parameters.docs.storyDescription} />
)}
<Preview withToolbar={withToolbar}>
<Story id={id} />
</Preview>
</Anchor>
);
import { Primary } from './Primary';
import { Props } from './Props';
import { Stories } from './Stories';
export const DocsPage: FunctionComponent<DocsPageProps> = ({
titleSlot = defaultTitleSlot,
subtitleSlot = defaultSubtitleSlot,
descriptionSlot = defaultDescriptionSlot,
primarySlot = defaultPrimarySlot,
propsSlot = defaultPropsSlot,
storiesSlot = defaultStoriesSlot,
titleSlot,
subtitleSlot,
descriptionSlot,
primarySlot,
propsSlot,
storiesSlot,
}) => (
<DocsContext.Consumer>
{context => {
const title = titleSlot(context) || '';
const subtitle = subtitleSlot(context) || '';
const description = descriptionSlot(context) || '';
const propsTableProps = propsSlot(context);
const { selectedKind, storyStore } = context;
const componentStories = storyStore
.getStoriesForKind(selectedKind)
.filter((s: any) => !(s.parameters && s.parameters.docs && s.parameters.docs.disable));
const primary = primarySlot(componentStories, context);
const stories = storiesSlot(componentStories, context);
return (
<PureDocsPage title={title} subtitle={subtitle}>
<Description markdown={description} />
{primary && <DocsStory key={primary.id} {...primary} expanded={false} withToolbar />}
{propsTableProps && <PropsTable {...propsTableProps} />}
{stories && stories.length > 0 && <StoriesHeading>Stories</StoriesHeading>}
{stories &&
stories.map(story => story && <DocsStory key={story.id} {...story} expanded />)}
</PureDocsPage>
);
}}
</DocsContext.Consumer>
<>
<Title slot={titleSlot} />
<Subtitle slot={subtitleSlot} />
<Description slot={descriptionSlot} />
<Primary slot={primarySlot} />
<Props slot={propsSlot} />
<Stories slot={storiesSlot} />
</>
);

View File

@ -0,0 +1,25 @@
import React, { FunctionComponent } from 'react';
import { Subheading } from './Subheading';
import { DocsStoryProps } from './shared';
import { Anchor } from './Anchor';
import { Description } from './Description';
import { Story } from './Story';
import { Preview } from './Preview';
export const DocsStory: FunctionComponent<DocsStoryProps> = ({
id,
name,
expanded = true,
withToolbar = false,
parameters,
}) => (
<Anchor storyId={id}>
{expanded && <Subheading>{name}</Subheading>}
{expanded && parameters && parameters.docs && parameters.docs.storyDescription && (
<Description markdown={parameters.docs.storyDescription} />
)}
<Preview withToolbar={withToolbar}>
<Story id={id} />
</Preview>
</Anchor>
);

View File

@ -0,0 +1,7 @@
import React, { FunctionComponent } from 'react';
import { H2 } from '@storybook/components/html';
interface HeadingProps {
children: JSX.Element | string;
}
export const Heading: FunctionComponent<HeadingProps> = ({ children }) => <H2>{children}</H2>;

View File

@ -0,0 +1,16 @@
import React, { useContext, FunctionComponent } from 'react';
import { DocsContext } from './DocsContext';
import { DocsStory } from './DocsStory';
import { getDocsStories } from './utils';
import { StorySlot } from './shared';
interface PrimaryProps {
slot?: StorySlot;
}
export const Primary: FunctionComponent<PrimaryProps> = ({ slot }) => {
const context = useContext(DocsContext);
const componentStories = getDocsStories(context);
const story = slot ? slot(componentStories, context) : componentStories && componentStories[0];
return story ? <DocsStory {...story} expanded={false} withToolbar /> : null;
};

View File

@ -1,15 +1,28 @@
import React, { FunctionComponent } from 'react';
import { PropsTable, PropsTableError, PropsTableProps } from '@storybook/components';
import { DocsContext, DocsContextProps } from './DocsContext';
import { Component, CURRENT_SELECTION } from './shared';
import React, { FunctionComponent, useContext } from 'react';
import { isNil } from 'lodash';
import { PropsExtractor } from '../lib/docgenUtils';
import {
PropsTable,
PropsTableError,
PropsTableProps,
PropDef,
TabsState,
} from '@storybook/components';
import { DocsContext, DocsContextProps } from './DocsContext';
import { Component, PropsSlot, CURRENT_SELECTION } from './shared';
import { getComponentName } from './utils';
import { PropsExtractor } from '../lib/docgen/types';
import { extractProps as reactExtractProps } from '../frameworks/react/extractProps';
import { extractProps as vueExtractProps } from '../frameworks/vue/extractProps';
interface PropsProps {
exclude?: string[];
of: '.' | Component;
of?: '.' | Component;
components?: {
[label: string]: Component;
};
slot?: PropsSlot;
}
// FIXME: remove in SB6.0 & require config
@ -24,36 +37,97 @@ const inferPropsExtractor = (framework: string): PropsExtractor | null => {
}
};
export const getPropsTableProps = (
{ exclude, of }: PropsProps,
export const getComponentProps = (
component: Component,
{ exclude }: PropsProps,
{ parameters }: DocsContextProps
): PropsTableProps => {
if (!component) {
return null;
}
try {
const params = parameters || {};
const { component, framework = null } = params;
const target = of === CURRENT_SELECTION ? component : of;
if (!target) {
throw new Error(PropsTableError.NO_COMPONENT);
}
const { framework = null } = params;
const { extractProps = inferPropsExtractor(framework) } = params.docs || {};
if (!extractProps) {
throw new Error(PropsTableError.PROPS_UNSUPPORTED);
}
return extractProps(target, { exclude });
let { rows } = extractProps(component);
if (!isNil(exclude)) {
rows = rows.filter((row: PropDef) => !exclude.includes(row.name));
}
return { rows };
} catch (err) {
return { error: err.message };
}
};
const PropsContainer: FunctionComponent<PropsProps> = props => (
<DocsContext.Consumer>
{context => {
const propsTableProps = getPropsTableProps(props, context);
return <PropsTable {...propsTableProps} />;
}}
</DocsContext.Consumer>
);
export const getComponent = (props: PropsProps = {}, context: DocsContextProps): Component => {
const { of } = props;
const { parameters = {} } = context;
const { component } = parameters;
const target = of === CURRENT_SELECTION ? component : of;
if (!target) {
if (of === CURRENT_SELECTION) {
return null;
}
throw new Error(PropsTableError.NO_COMPONENT);
}
return target;
};
const PropsContainer: FunctionComponent<PropsProps> = props => {
const context = useContext(DocsContext);
const { slot, components } = props;
const {
parameters: { subcomponents },
} = context;
let allComponents = components;
if (!allComponents) {
const main = getComponent(props, context);
const mainLabel = getComponentName(main);
const mainProps = slot ? slot(context, main) : getComponentProps(main, props, context);
if (!subcomponents || typeof subcomponents !== 'object') {
return mainProps && <PropsTable {...mainProps} />;
}
allComponents = { [mainLabel]: main, ...subcomponents };
}
const tabs: { label: string; table: PropsTableProps }[] = [];
Object.entries(allComponents).forEach(([label, component]) => {
tabs.push({
label,
table: slot ? slot(context, component) : getComponentProps(component, props, context),
});
});
return (
<TabsState>
{tabs.map(({ label, table }) => {
if (!table) {
return null;
}
const id = `prop_table_div_${label}`;
return (
<div key={id} id={id} title={label}>
{({ active }: { active: boolean }) =>
active ? <PropsTable key={`prop_table_${label}`} {...table} /> : null
}
</div>
);
})}
</TabsState>
);
};
PropsContainer.defaultProps = {
of: '.',
};
export { PropsContainer as Props };

View File

@ -0,0 +1,33 @@
import React, { useContext, FunctionComponent } from 'react';
import { DocsContext } from './DocsContext';
import { DocsStory } from './DocsStory';
import { Heading } from './Heading';
import { getDocsStories } from './utils';
import { StoriesSlot, DocsStoryProps } from './shared';
interface StoriesProps {
slot?: StoriesSlot;
title?: JSX.Element | string;
}
export const Stories: FunctionComponent<StoriesProps> = ({ slot, title }) => {
const context = useContext(DocsContext);
const componentStories = getDocsStories(context);
const stories: DocsStoryProps[] = slot
? slot(componentStories, context)
: componentStories && componentStories.slice(1);
if (!stories) {
return null;
}
return (
<>
<Heading>{title}</Heading>
{stories.map(story => story && <DocsStory key={story.id} {...story} expanded />)}
</>
);
};
Stories.defaultProps = {
title: 'Stories',
};

View File

@ -0,0 +1,7 @@
import React, { FunctionComponent } from 'react';
import { H3 } from '@storybook/components/html';
interface SubheadingProps {
children: JSX.Element | string;
}
export const Subheading: FunctionComponent<SubheadingProps> = ({ children }) => <H3>{children}</H3>;

View File

@ -0,0 +1,19 @@
import React, { useContext, FunctionComponent } from 'react';
import { Subtitle as PureSubtitle } from '@storybook/components';
import { DocsContext } from './DocsContext';
import { StringSlot } from './shared';
interface SubtitleProps {
slot?: StringSlot;
children?: JSX.Element | string;
}
export const Subtitle: FunctionComponent<SubtitleProps> = ({ slot, children }) => {
const context = useContext(DocsContext);
const { parameters } = context;
let text: JSX.Element | string = children;
if (!text) {
text = slot ? slot(context) : parameters && parameters.componentSubtitle;
}
return text ? <PureSubtitle className="sbdocs-subtitle">{text}</PureSubtitle> : null;
};

View File

@ -0,0 +1,48 @@
import React, { useContext, FunctionComponent } from 'react';
import { parseKind } from '@storybook/router';
import { Title as PureTitle } from '@storybook/components';
import { DocsContext } from './DocsContext';
import { StringSlot } from './shared';
interface TitleProps {
slot?: StringSlot;
children?: JSX.Element | string;
}
export const defaultTitleSlot: StringSlot = ({ selectedKind, parameters }) => {
const {
showRoots,
hierarchyRootSeparator: rootSeparator,
hierarchySeparator: groupSeparator,
} = (parameters && parameters.options) || {
showRoots: undefined,
hierarchyRootSeparator: '|',
hierarchySeparator: /\/|\./,
};
let groups;
if (typeof showRoots !== 'undefined') {
groups = selectedKind.split('/');
} else {
// This covers off all the remaining cases:
// - If the separators were set above, we should use them
// - If they weren't set, we should only should use the old defaults if the kind contains '.' or '|',
// which for this particular splitting is the only case in which it actually matters.
({ groups } = parseKind(selectedKind, { rootSeparator, groupSeparator }));
}
return (groups && groups[groups.length - 1]) || selectedKind;
};
export const Title: FunctionComponent<TitleProps> = ({ slot, children }) => {
const context = useContext(DocsContext);
const { selectedKind, parameters } = context;
let text: JSX.Element | string = children;
if (!text) {
if (slot) {
text = slot(context);
} else {
text = defaultTitleSlot({ selectedKind, parameters });
}
}
return text ? <PureTitle className="sbdocs-title">{text}</PureTitle> : null;
};

View File

@ -5,12 +5,21 @@ export * from './Description';
export * from './DocsContext';
export * from './DocsPage';
export * from './DocsContainer';
export * from './DocsStory';
export * from './Heading';
export * from './Meta';
export * from './Preview';
export * from './Primary';
export * from './Props';
export * from './Source';
export * from './Stories';
export * from './Story';
export * from './Subheading';
export * from './Subtitle';
export * from './Title';
export * from './Wrapper';
export * from './shared';
// helper function for MDX
export const makeStoryFn = (val: any) => (typeof val === 'function' ? val : () => val);

View File

@ -1,2 +1,41 @@
import { PropsTableProps } from '@storybook/components';
export const CURRENT_SELECTION = '.';
export type Component = any;
export interface StoryData {
id?: string;
kind?: string;
name?: string;
parameters?: any;
}
export type DocsStoryProps = StoryData & {
expanded?: boolean;
withToolbar?: boolean;
};
export interface SlotContext {
id?: string;
selectedKind?: string;
selectedStory?: string;
parameters?: any;
storyStore?: any;
}
export type StringSlot = (context: SlotContext) => string;
export type DescriptionSlot = (description: string, context: SlotContext) => string;
export type PropsSlot = (context: SlotContext, component: Component) => PropsTableProps;
export type StorySlot = (stories: StoryData[], context: SlotContext) => DocsStoryProps;
export type StoriesSlot = (stories: StoryData[], context: SlotContext) => DocsStoryProps[];
export interface DocsPageProps {
titleSlot?: StringSlot;
subtitleSlot?: StringSlot;
descriptionSlot?: DescriptionSlot;
primarySlot?: StorySlot;
propsSlot?: PropsSlot;
storiesSlot?: StoriesSlot;
}

View File

@ -0,0 +1,34 @@
/* eslint-disable no-underscore-dangle */
import { DocsContextProps } from './DocsContext';
import { StoryData, Component } from './shared';
export const getDocsStories = (context: DocsContextProps): StoryData[] => {
const { storyStore, selectedKind } = context;
return storyStore
.getStoriesForKind(selectedKind)
.filter((s: any) => !(s.parameters && s.parameters.docs && s.parameters.docs.disable));
};
const titleCase = (str: string): string =>
str
.split('-')
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
.join('');
export const getComponentName = (component: Component): string => {
if (!component) {
return undefined;
}
if (typeof component === 'string') {
if (component.includes('-')) {
return titleCase(component);
}
return component;
}
if (component.__docgenInfo && component.__docgenInfo.displayName) {
return component.__docgenInfo.displayName;
}
return component.name;
};

View File

@ -104,10 +104,10 @@ export const extractProps = (component: Component) => {
data.forEach((item: Method | Property) => {
const sectionItem: PropDef = {
name: item.name,
type: { name: isMethod(item) ? displaySignature(item) : item.type },
type: { summary: isMethod(item) ? displaySignature(item) : item.type },
required: isMethod(item) ? false : !item.optional,
description: item.description,
defaultValue: isMethod(item) ? '' : item.defaultValue,
defaultValue: { summary: isMethod(item) ? '' : item.defaultValue },
};
const section = mapItemToSection(key, item);

View File

@ -1 +1 @@
export * from '../../lib/docgenUtils';
export * from '../../lib/docgen';

View File

@ -1,25 +1,17 @@
import fs from 'fs';
import * as common from './preset';
import deprecate from 'util-deprecate';
import dedent from 'ts-dedent';
import * as common from '../../preset';
const makePreset = (framework: string) => {
const docsConfig = [`${__dirname}/config.js`];
const frameworkConfig = `${__dirname}/../../../dist/frameworks/${framework}/config.js`;
if (fs.existsSync(frameworkConfig)) {
docsConfig.push(frameworkConfig);
}
function config(entry: any[] = []) {
return [...docsConfig, ...entry];
}
deprecate(
() => {},
dedent`
Framework-specific presets are no longer-needed as of Storybook 5.3 and will be removed in 6.0.
const configureJSX = framework !== 'react';
const webpack = (webpackConfig: any, options: any) =>
common.webpack(webpackConfig, { configureJSX, ...options });
return {
...common,
webpack,
config,
};
Please use '@storybook/addon-docs/preset' instead of '@storybook/addon-docs/${framework}/preset'.
`
)();
return common;
};
export default makePreset;

View File

@ -20,7 +20,11 @@ export function webpack(webpackConfig: any = {}, options: any = {}) {
const { module = {} } = webpackConfig;
// it will reuse babel options that are already in use in storybook
// also, these babel options are chained with other presets.
const { babelOptions, configureJSX, sourceLoaderOptions = {} } = options;
const {
babelOptions,
configureJSX = options.framework !== 'react', // if not user-specified
sourceLoaderOptions = {},
} = options;
// set `sourceLoaderOptions` to `null` to disable for manual configuration
const sourceLoader = sourceLoaderOptions
@ -34,7 +38,7 @@ export function webpack(webpackConfig: any = {}, options: any = {}) {
]
: [];
return {
const result = {
...webpackConfig,
module: {
...module,
@ -72,8 +76,20 @@ export function webpack(webpackConfig: any = {}, options: any = {}) {
],
},
};
return result;
}
export function addons(entry: any[] = []) {
export function addons(entry: any[] = [], options: any) {
return [...entry, require.resolve('../../register')];
}
export function config(entry: any[] = [], options: any = {}) {
const { framework } = options;
const docsConfig = [require.resolve('./config')];
try {
docsConfig.push(require.resolve(`../${framework}/config`));
} catch (err) {
// there is no custom config for the user's framework, do nothing
}
return [...docsConfig, ...entry];
}

View File

@ -2,7 +2,7 @@
import { addParameters } from '@storybook/client-api';
import { StoryFn } from '@storybook/addons';
import { extractProps } from './extractProps';
import { extractComponentDescription } from '../../lib/docgenUtils';
import { extractComponentDescription } from '../../lib/docgen';
addParameters({
docs: {

View File

@ -1,12 +1,9 @@
import PropTypes from 'prop-types';
import { isForwardRef, isMemo } from 'react-is';
import { PropDef } from '@storybook/components';
import {
PropDefGetter,
PropsExtractor,
extractPropsFromDocgen,
hasDocgen,
} from '../../lib/docgenUtils';
import { hasDocgen, extractPropsFromDocgen, PropsExtractor, TypeSystem } from '../../lib/docgen';
import { Component } from '../../blocks/shared';
import { enhancePropTypesProps } from './propTypes/handleProp';
export interface PropDefMap {
[p: string]: PropDef;
@ -22,21 +19,30 @@ Object.keys(PropTypes).forEach(typeName => {
propTypesMap.set(type.isRequired, typeName);
});
export const getPropDefs: PropDefGetter = (type, section) => {
let processedType = type;
function getPropDefs(component: Component, section: string): PropDef[] {
let processedComponent = component;
// eslint-disable-next-line react/forbid-foreign-prop-types
if (!hasDocgen(type) && !type.propTypes) {
if (isForwardRef(type) || type.render) {
processedType = type.render().type;
if (!hasDocgen(component) && !component.propTypes) {
if (isForwardRef(component) || component.render) {
processedComponent = component.render().type;
}
if (isMemo(type)) {
processedType = type.type().type;
if (isMemo(component)) {
processedComponent = component.type().type;
}
}
return extractPropsFromDocgen(processedType, section);
};
const extractedProps = extractPropsFromDocgen(processedComponent, section);
if (extractedProps.length === 0) {
return [];
}
if (extractedProps[0].typeSystem === TypeSystem.JAVASCRIPT) {
return enhancePropTypesProps(extractedProps, component);
}
return extractedProps.map(x => x.propDef);
}
export const extractProps: PropsExtractor = component => ({
rows: getPropDefs(component, 'props'),

View File

@ -0,0 +1,199 @@
import { parse } from './acornParser';
import {
InspectionType,
InspectionElement,
InspectionObject,
InspectionArray,
InspectionIdentifier,
InspectionLiteral,
InspectionFunction,
InspectionUnknown,
} from './types';
describe('parse', () => {
describe('expression', () => {
it('support HTML element', () => {
const result = parse('<div>Hello!</div>');
const inferedType = result.inferedType as InspectionElement;
expect(inferedType.type).toBe(InspectionType.ELEMENT);
expect(inferedType.identifier).toBe('div');
expect(result.ast).toBeDefined();
});
it('support React declaration', () => {
const result = parse('<FunctionalComponent />');
const inferedType = result.inferedType as InspectionElement;
expect(inferedType.type).toBe(InspectionType.ELEMENT);
expect(inferedType.identifier).toBe('FunctionalComponent');
expect(result.ast).toBeDefined();
});
it('support anonymous functional React component', () => {
const result = parse('() => { return <div>Hey!</div>; }');
const inferedType = result.inferedType as InspectionElement;
expect(inferedType.type).toBe(InspectionType.ELEMENT);
expect(inferedType.identifier).toBeUndefined();
expect(result.ast).toBeDefined();
});
it('support named functional React component', () => {
const result = parse('function NamedFunctionalComponent() { return <div>Hey!</div>; }');
const inferedType = result.inferedType as InspectionElement;
expect(inferedType.type).toBe(InspectionType.ELEMENT);
expect(inferedType.identifier).toBe('NamedFunctionalComponent');
expect(result.ast).toBeDefined();
});
it('support class React component', () => {
const result = parse(`
class ClassComponent extends React.PureComponent {
render() {
return <div>Hey!</div>;
}
}`);
const inferedType = result.inferedType as InspectionElement;
expect(inferedType.type).toBe(InspectionType.ELEMENT);
expect(inferedType.identifier).toBe('ClassComponent');
expect(result.ast).toBeDefined();
});
it('support PropTypes.shape', () => {
const result = parse('PropTypes.shape({ foo: PropTypes.string })');
const inferedType = result.inferedType as InspectionObject;
expect(inferedType.type).toBe(InspectionType.OBJECT);
expect(result.ast).toBeDefined();
});
it('support shape', () => {
const result = parse('shape({ foo: PropTypes.string })');
const inferedType = result.inferedType as InspectionObject;
expect(inferedType.type).toBe(InspectionType.OBJECT);
expect(result.ast).toBeDefined();
});
it('support single prop object literal', () => {
const result = parse('{ foo: PropTypes.string }');
const inferedType = result.inferedType as InspectionObject;
expect(inferedType.type).toBe(InspectionType.OBJECT);
expect(result.ast).toBeDefined();
});
it('support multi prop object literal', () => {
const result = parse(`
{
foo: PropTypes.string,
bar: PropTypes.string
}`);
const inferedType = result.inferedType as InspectionObject;
expect(inferedType.type).toBe(InspectionType.OBJECT);
expect(result.ast).toBeDefined();
});
it('support required prop', () => {
const result = parse('{ foo: PropTypes.string.isRequired }');
const inferedType = result.inferedType as InspectionObject;
expect(inferedType.type).toBe(InspectionType.OBJECT);
expect(result.ast).toBeDefined();
});
it('support array', () => {
const result = parse("['bottom-left', 'botton-center', 'bottom-right']");
const inferedType = result.inferedType as InspectionArray;
expect(inferedType.type).toBe(InspectionType.ARRAY);
expect(result.ast).toBeDefined();
});
it('support object identifier', () => {
const result = parse('NAMED_OBJECT');
const inferedType = result.inferedType as InspectionIdentifier;
expect(inferedType.type).toBe(InspectionType.IDENTIFIER);
expect(inferedType.identifier).toBe('NAMED_OBJECT');
expect(result.ast).toBeDefined();
});
it('support anonymous function', () => {
const result = parse('() => {}');
const inferedType = result.inferedType as InspectionFunction;
expect(inferedType.type).toBe(InspectionType.FUNCTION);
expect(inferedType.identifier).toBeUndefined();
expect(inferedType.hasArguments).toBeFalsy();
expect(result.ast).toBeDefined();
});
it('support anonymous function with arguments', () => {
const result = parse('(a, b) => {}');
const inferedType = result.inferedType as InspectionFunction;
expect(inferedType.type).toBe(InspectionType.FUNCTION);
expect(inferedType.identifier).toBeUndefined();
expect(inferedType.hasArguments).toBeTruthy();
expect(result.ast).toBeDefined();
});
it('support named function', () => {
const result = parse('function concat() {}');
const inferedType = result.inferedType as InspectionFunction;
expect(inferedType.type).toBe(InspectionType.FUNCTION);
expect(inferedType.identifier).toBe('concat');
expect(inferedType.hasArguments).toBeFalsy();
expect(result.ast).toBeDefined();
});
it('support named function with arguments', () => {
const result = parse('function concat(a, b) {}');
const inferedType = result.inferedType as InspectionFunction;
expect(inferedType.type).toBe(InspectionType.FUNCTION);
expect(inferedType.identifier).toBe('concat');
expect(inferedType.hasArguments).toBeTruthy();
expect(result.ast).toBeDefined();
});
it('support class', () => {
const result = parse('class Foo {}');
const inferedType = result.inferedType as InspectionFunction;
expect(inferedType.type).toBe(InspectionType.CLASS);
expect(inferedType.identifier).toBe('Foo');
expect(result.ast).toBeDefined();
});
[
{ name: 'string', value: "'string value'" },
{ name: 'numeric', value: '1' },
{ name: 'boolean (true)', value: 'true' },
{ name: 'boolean (false)', value: 'false' },
{ name: 'null', value: 'null' },
].forEach(x => {
it(`support ${x.name}`, () => {
const result = parse(x.value);
const inferedType = result.inferedType as InspectionLiteral;
expect(inferedType.type).toBe(InspectionType.LITERAL);
expect(result.ast).toBeDefined();
});
});
it("returns Unknown when it's not supported", () => {
const result = parse("Symbol('foo')");
const inferedType = result.inferedType as InspectionUnknown;
expect(inferedType.type).toBe(InspectionType.UNKNOWN);
expect(result.ast).toBeDefined();
});
});
});

View File

@ -0,0 +1,211 @@
import { Parser } from 'acorn';
// @ts-ignore
import jsx from 'acorn-jsx';
import { isNil } from 'lodash';
import estree from 'estree';
// @ts-ignore
import * as acornWalk from 'acorn-walk';
import {
InspectionType,
InspectionInferedType,
InspectionLiteral,
InspectionElement,
InspectionFunction,
InspectionClass,
InspectionObject,
InspectionUnknown,
InspectionIdentifier,
InspectionArray,
} from './types';
interface ParsingResult<T> {
inferedType: T;
ast: any;
}
const ACORN_WALK_VISITORS = {
...acornWalk.base,
JSXElement: () => {},
};
const acornParser = Parser.extend(jsx());
// Cannot use "estree.Identifier" type because this function also support "JSXIdentifier".
function extractIdentifierName(identifierNode: any) {
return !isNil(identifierNode) ? identifierNode.name : null;
}
function parseIdentifier(identifierNode: estree.Identifier): ParsingResult<InspectionIdentifier> {
return {
inferedType: {
type: InspectionType.IDENTIFIER,
identifier: extractIdentifierName(identifierNode),
},
ast: identifierNode,
};
}
function parseLiteral(literalNode: estree.Literal): ParsingResult<InspectionLiteral> {
return {
inferedType: { type: InspectionType.LITERAL },
ast: literalNode,
};
}
function parseFunction(
funcNode: estree.FunctionExpression | estree.ArrowFunctionExpression
): ParsingResult<InspectionFunction | InspectionElement> {
let innerJsxElementNode;
// If there is at least a JSXElement in the body of the function, then it's a React component.
acornWalk.simple(
funcNode.body,
{
JSXElement(node: any) {
innerJsxElementNode = node;
},
},
ACORN_WALK_VISITORS
);
const isJsx = !isNil(innerJsxElementNode);
const inferedType: InspectionFunction | InspectionElement = {
type: isJsx ? InspectionType.ELEMENT : InspectionType.FUNCTION,
hasArguments: funcNode.params.length !== 0,
};
const identifierName = extractIdentifierName((funcNode as estree.FunctionExpression).id);
if (!isNil(identifierName)) {
inferedType.identifier = identifierName;
}
return {
inferedType,
ast: funcNode,
};
}
function parseClass(
classNode: estree.ClassExpression
): ParsingResult<InspectionClass | InspectionElement> {
let innerJsxElementNode;
// If there is at least a JSXElement in the body of the class, then it's a React component.
acornWalk.simple(
classNode.body,
{
JSXElement(node: any) {
innerJsxElementNode = node;
},
},
ACORN_WALK_VISITORS
);
const inferedType: any = {
type: !isNil(innerJsxElementNode) ? InspectionType.ELEMENT : InspectionType.CLASS,
identifier: extractIdentifierName(classNode.id),
};
return {
inferedType,
ast: classNode,
};
}
function parseJsxElement(jsxElementNode: any): ParsingResult<InspectionElement> {
const inferedType: InspectionElement = {
type: InspectionType.ELEMENT,
};
const identifierName = extractIdentifierName(jsxElementNode.openingElement.name);
if (!isNil(identifierName)) {
inferedType.identifier = identifierName;
}
return {
inferedType,
ast: jsxElementNode,
};
}
function parseCall(callNode: estree.CallExpression): ParsingResult<InspectionObject> {
const identifierNode =
callNode.callee.type === 'MemberExpression' ? callNode.callee.property : callNode.callee;
const identifierName = extractIdentifierName(identifierNode);
if (identifierName === 'shape') {
return {
inferedType: { type: InspectionType.OBJECT },
ast: callNode.arguments[0],
};
}
return null;
}
function parseObject(objectNode: estree.ObjectExpression): ParsingResult<InspectionObject> {
return {
inferedType: { type: InspectionType.OBJECT },
ast: objectNode,
};
}
function parseArray(arrayNode: estree.ArrayExpression): ParsingResult<InspectionArray> {
return {
inferedType: { type: InspectionType.ARRAY },
ast: arrayNode,
};
}
// Cannot set "expression" type to "estree.Expression" because the type doesn't include JSX.
function parseExpression(expression: any): ParsingResult<InspectionInferedType> {
switch (expression.type) {
case 'Identifier':
return parseIdentifier(expression);
case 'Literal':
return parseLiteral(expression);
case 'FunctionExpression':
case 'ArrowFunctionExpression':
return parseFunction(expression);
case 'ClassExpression':
return parseClass(expression);
case 'JSXElement':
return parseJsxElement(expression);
case 'CallExpression':
return parseCall(expression);
case 'ObjectExpression':
return parseObject(expression);
case 'ArrayExpression':
return parseArray(expression);
default:
return null;
}
}
export function parse(value: string): ParsingResult<InspectionInferedType> {
const ast = (acornParser.parse(`(${value})`) as unknown) as estree.Program;
let parsingResult: ParsingResult<InspectionUnknown> = {
inferedType: { type: InspectionType.UNKNOWN },
ast,
};
if (!isNil(ast.body[0])) {
const rootNode = ast.body[0];
switch (rootNode.type) {
case 'ExpressionStatement': {
const expressionResult = parseExpression(rootNode.expression);
if (!isNil(expressionResult)) {
parsingResult = expressionResult as any;
}
break;
}
default:
break;
}
}
return parsingResult;
}

View File

@ -0,0 +1,14 @@
import { parse } from './acornParser';
import { InspectionResult, InspectionType } from './types';
export function inspectValue(value: string): InspectionResult {
try {
const parsingResult = parse(value);
return { ...parsingResult };
} catch (e) {
// do nothing.
}
return { inferedType: { type: InspectionType.UNKNOWN } };
}

View File

@ -0,0 +1,79 @@
export enum InspectionType {
IDENTIFIER = 'Identifier',
LITERAL = 'Literal',
OBJECT = 'Object',
ARRAY = 'Array',
FUNCTION = 'Function',
CLASS = 'Class',
ELEMENT = 'Element',
UNKNOWN = 'Unknown',
}
export interface BaseInspectionInferedType {
type: InspectionType;
}
// TODO: Fix this.
// export interface OptionalIdentifierInspectionType extends BaseInspectionInferedType {
// identifier?: string;
// }
// export interface RequiredIdentifierInspectionType extends BaseInspectionInferedType {
// identifier: string;
// }
// export type IdentifiableInspectionType =
// | OptionalIdentifierInspectionType
// | RequiredIdentifierInspectionType;
export interface InspectionIdentifier extends BaseInspectionInferedType {
type: InspectionType.IDENTIFIER;
identifier: string;
}
export interface InspectionLiteral extends BaseInspectionInferedType {
type: InspectionType.LITERAL;
}
export interface InspectionObject extends BaseInspectionInferedType {
type: InspectionType.OBJECT;
}
export interface InspectionArray extends BaseInspectionInferedType {
type: InspectionType.ARRAY;
}
export interface InspectionClass extends BaseInspectionInferedType {
type: InspectionType.CLASS;
identifier: string;
}
export interface InspectionFunction extends BaseInspectionInferedType {
type: InspectionType.FUNCTION;
identifier?: string;
hasArguments: boolean;
}
export interface InspectionElement extends BaseInspectionInferedType {
type: InspectionType.ELEMENT;
identifier?: string;
}
export interface InspectionUnknown extends BaseInspectionInferedType {
type: InspectionType.UNKNOWN;
}
export type InspectionInferedType =
| InspectionIdentifier
| InspectionLiteral
| InspectionObject
| InspectionArray
| InspectionClass
| InspectionFunction
| InspectionElement
| InspectionUnknown;
export interface InspectionResult {
inferedType: InspectionInferedType;
ast?: any;
}

View File

@ -0,0 +1,6 @@
export const CUSTOM_CAPTION = 'custom';
export const OBJECT_CAPTION = 'object';
export const ARRAY_CAPTION = 'array';
export const CLASS_CAPTION = 'class';
export const FUNCTION_CAPTION = 'func';
export const ELEMENT_CAPTION = 'element';

View File

@ -0,0 +1,115 @@
import { isNil } from 'lodash';
// @ts-ignore
import { PropDefaultValue, PropSummaryValue } from '@storybook/components';
import { inspectValue } from '../inspection/inspectValue';
import { OBJECT_CAPTION, FUNCTION_CAPTION, ELEMENT_CAPTION, ARRAY_CAPTION } from './captions';
import { generateCode } from './generateCode';
import {
InspectionFunction,
InspectionResult,
InspectionType,
InspectionElement,
} from '../inspection/types';
import { isHtmlTag } from './isHtmlTag';
const MAX_SUMMARY_LENGTH = 50;
function isTooLongForSummary(value: string): boolean {
return value.length > MAX_SUMMARY_LENGTH;
}
// TODO: Fix this any type.
function getPrettyIdentifier(inferedType: any): string {
const { type, identifier } = inferedType;
switch (type) {
case InspectionType.FUNCTION:
return inferedType.hasArguments ? `${identifier}( ... )` : `${identifier}()`;
case InspectionType.ELEMENT:
return `<${identifier} />`;
default:
return identifier;
}
}
function createSummaryValue(summary: string, detail?: string): PropSummaryValue {
return { summary, detail };
}
function generateObject({ ast }: InspectionResult): PropDefaultValue {
let prettyCaption = generateCode(ast, true);
// Cannot get escodegen to add a space before the last } with the compact mode settings.
// This fix it until a better solution is found.
if (!prettyCaption.endsWith(' }')) {
prettyCaption = `${prettyCaption.slice(0, -1)} }`;
}
return !isTooLongForSummary(prettyCaption)
? createSummaryValue(prettyCaption)
: createSummaryValue(OBJECT_CAPTION, generateCode(ast));
}
function generateFunc({ inferedType, ast }: InspectionResult): PropDefaultValue {
const { identifier } = inferedType as InspectionFunction;
if (!isNil(identifier)) {
return createSummaryValue(getPrettyIdentifier(inferedType), generateCode(ast));
}
const prettyCaption = generateCode(ast, true);
return !isTooLongForSummary(prettyCaption)
? createSummaryValue(prettyCaption)
: createSummaryValue(FUNCTION_CAPTION, generateCode(ast));
}
// All elements are JSX elements.
// JSX elements are not supported by escodegen.
function generateElement(
defaultValue: string,
inspectionResult: InspectionResult
): PropDefaultValue {
const { inferedType } = inspectionResult;
const { identifier } = inferedType as InspectionElement;
if (!isNil(identifier)) {
if (!isHtmlTag(identifier)) {
const prettyIdentifier = getPrettyIdentifier(inferedType);
return createSummaryValue(
prettyIdentifier,
prettyIdentifier !== defaultValue ? defaultValue : undefined
);
}
}
return !isTooLongForSummary(defaultValue)
? createSummaryValue(defaultValue)
: createSummaryValue(ELEMENT_CAPTION, defaultValue);
}
function generateArray({ ast }: InspectionResult): PropDefaultValue {
const prettyCaption = generateCode(ast, true);
return !isTooLongForSummary(prettyCaption)
? createSummaryValue(prettyCaption)
: createSummaryValue(ARRAY_CAPTION, generateCode(ast));
}
export function createDefaultValue(defaultValue: string): PropDefaultValue {
const inspectionResult = inspectValue(defaultValue);
switch (inspectionResult.inferedType.type) {
case InspectionType.OBJECT:
return generateObject(inspectionResult);
case InspectionType.FUNCTION:
return generateFunc(inspectionResult);
case InspectionType.ELEMENT:
return generateElement(defaultValue, inspectionResult);
case InspectionType.ARRAY:
return generateArray(inspectionResult);
default:
return null;
}
}

View File

@ -0,0 +1,366 @@
import { isNil } from 'lodash';
import { PropSummaryValue, PropType } from '@storybook/components';
import { ExtractedProp, DocgenPropType } from '../../../lib/docgen';
import { inspectValue } from '../inspection/inspectValue';
import { generateCode } from './generateCode';
import { generateFuncSignature } from './generateFuncSignature';
import {
OBJECT_CAPTION,
ARRAY_CAPTION,
CLASS_CAPTION,
FUNCTION_CAPTION,
ELEMENT_CAPTION,
CUSTOM_CAPTION,
} from './captions';
import { InspectionType } from '../inspection/types';
import { isHtmlTag } from './isHtmlTag';
const MAX_SUMMARY_LENGTH = 35;
enum PropTypesType {
CUSTOM = 'custom',
ANY = 'any',
FUNC = 'func',
SHAPE = 'shape',
OBJECT = 'object',
INSTANCEOF = 'instanceOf',
OBJECTOF = 'objectOf',
UNION = 'union',
ENUM = 'enum',
ARRAYOF = 'arrayOf',
ELEMENT = 'element',
ELEMENTTYPE = 'elementType',
NODE = 'node',
}
interface EnumValue {
value: string;
computed: boolean;
}
interface TypeDef {
name: string;
value: PropSummaryValue;
inferedType?: InspectionType;
}
function createTypeDef({
name,
summary,
detail,
inferedType,
}: {
name: string;
summary: string;
detail?: string;
inferedType?: InspectionType;
}): TypeDef {
return {
name,
value: {
summary,
detail: !isNil(detail) ? detail : summary,
},
inferedType,
};
}
function cleanPropTypes(value: string): string {
return value.replace(/PropTypes./g, '').replace(/.isRequired/g, '');
}
function prettyObject(ast: any, compact = false): string {
return cleanPropTypes(generateCode(ast, compact));
}
function getCaptionFromInspectionType(type: InspectionType): string {
switch (type) {
case InspectionType.OBJECT:
return OBJECT_CAPTION;
case InspectionType.ARRAY:
return ARRAY_CAPTION;
case InspectionType.CLASS:
return CLASS_CAPTION;
case InspectionType.FUNCTION:
return FUNCTION_CAPTION;
case InspectionType.ELEMENT:
return ELEMENT_CAPTION;
default:
return CUSTOM_CAPTION;
}
}
function isTooLongForSummary(value: string): boolean {
return value.length > MAX_SUMMARY_LENGTH;
}
function generateValuesForObjectAst(ast: any): [string, string] {
let summary = prettyObject(ast, true);
let detail;
if (!isTooLongForSummary(summary)) {
detail = summary;
} else {
summary = OBJECT_CAPTION;
detail = prettyObject(ast);
}
return [summary, detail];
}
function generateCustom({ raw }: DocgenPropType): TypeDef {
if (!isNil(raw)) {
const { inferedType, ast } = inspectValue(raw);
const { type, identifier } = inferedType as any;
let summary;
let detail;
switch (type) {
case InspectionType.IDENTIFIER:
case InspectionType.LITERAL:
summary = raw;
break;
case InspectionType.OBJECT: {
const [objectCaption, objectValue] = generateValuesForObjectAst(ast);
summary = objectCaption;
detail = objectValue;
break;
}
case InspectionType.ELEMENT:
summary = !isNil(identifier) && !isHtmlTag(identifier) ? identifier : ELEMENT_CAPTION;
detail = raw;
break;
default:
summary = getCaptionFromInspectionType(type);
detail = raw;
break;
}
return createTypeDef({
name: PropTypesType.CUSTOM,
summary,
detail,
inferedType: type,
});
}
return createTypeDef({ name: PropTypesType.CUSTOM, summary: CUSTOM_CAPTION });
}
function generateFunc(extractedProp: ExtractedProp): TypeDef {
const { jsDocTags } = extractedProp;
if (!isNil(jsDocTags)) {
if (!isNil(jsDocTags.params) || !isNil(jsDocTags.returns)) {
return createTypeDef({
name: PropTypesType.FUNC,
summary: FUNCTION_CAPTION,
detail: generateFuncSignature(jsDocTags.params, jsDocTags.returns),
});
}
}
return createTypeDef({ name: PropTypesType.FUNC, summary: FUNCTION_CAPTION });
}
function generateShape(type: DocgenPropType, extractedProp: ExtractedProp): TypeDef {
const fields = Object.keys(type.value)
.map((key: string) => `${key}: ${generateType(type.value[key], extractedProp).value.detail}`)
.join(', ');
const { ast } = inspectValue(`{ ${fields} }`);
const [summary, detail] = generateValuesForObjectAst(ast);
return createTypeDef({
name: PropTypesType.SHAPE,
summary,
detail,
});
}
function generateObjectOf(type: DocgenPropType, extractedProp: ExtractedProp): TypeDef {
const format = (of: string) => `objectOf(${of})`;
const { name, value } = generateType(type.value, extractedProp);
// eslint-disable-next-line prefer-const
let { summary, detail } = value;
if (name === PropTypesType.SHAPE) {
if (!isTooLongForSummary(detail)) {
summary = detail;
}
}
return createTypeDef({
name: PropTypesType.OBJECTOF,
summary: format(summary),
detail: format(detail),
});
}
function generateUnion(type: DocgenPropType, extractedProp: ExtractedProp): TypeDef {
if (Array.isArray(type.value)) {
const values = type.value.reduce(
(acc: any, v: any) => {
const { summary, detail } = generateType(v, extractedProp).value;
acc.summary.push(summary);
acc.detail.push(detail);
return acc;
},
{ summary: [], detail: [] }
);
return createTypeDef({
name: PropTypesType.UNION,
summary: values.summary.join(' | '),
detail: values.detail.join(' | '),
});
}
return createTypeDef({ name: PropTypesType.UNION, summary: type.value });
}
function generateEnumValue({ value, computed }: EnumValue): TypeDef {
if (computed) {
const { inferedType, ast } = inspectValue(value) as any;
const { type } = inferedType;
let caption = getCaptionFromInspectionType(type);
if (
type === InspectionType.FUNCTION ||
type === InspectionType.CLASS ||
type === InspectionType.ELEMENT
) {
if (!isNil(inferedType.identifier)) {
caption = inferedType.identifier;
}
}
return createTypeDef({
name: 'enumvalue',
summary: caption,
detail: type === InspectionType.OBJECT ? prettyObject(ast) : value,
inferedType: type,
});
}
return createTypeDef({ name: 'enumvalue', summary: value });
}
function generateEnum(type: DocgenPropType): TypeDef {
if (Array.isArray(type.value)) {
const values = type.value.reduce(
(acc: any, v: EnumValue) => {
const { summary, detail } = generateEnumValue(v).value;
acc.summary.push(summary);
acc.detail.push(detail);
return acc;
},
{ summary: [], detail: [] }
);
return createTypeDef({
name: PropTypesType.ENUM,
summary: values.summary.join(' | '),
detail: values.detail.join(' | '),
});
}
return createTypeDef({ name: PropTypesType.ENUM, summary: type.value });
}
function braceAfter(of: string): string {
return `${of}[]`;
}
function braceAround(of: string): string {
return `[${of}]`;
}
function createArrayOfObjectTypeDef(summary: string, detail: string): TypeDef {
return createTypeDef({
name: PropTypesType.ARRAYOF,
summary: summary === OBJECT_CAPTION ? braceAfter(summary) : braceAround(summary),
detail: braceAround(detail),
});
}
function generateArray(type: DocgenPropType, extractedProp: ExtractedProp): TypeDef {
const { name, value, inferedType } = generateType(type.value, extractedProp);
const { summary, detail } = value;
if (name === PropTypesType.CUSTOM) {
if (inferedType === InspectionType.OBJECT) {
return createArrayOfObjectTypeDef(summary, detail);
}
} else if (name === PropTypesType.SHAPE) {
return createArrayOfObjectTypeDef(summary, detail);
}
return createTypeDef({ name: PropTypesType.ARRAYOF, summary: braceAfter(detail) });
}
function generateType(type: DocgenPropType, extractedProp: ExtractedProp): TypeDef {
try {
switch (type.name) {
case PropTypesType.CUSTOM:
return generateCustom(type);
case PropTypesType.FUNC:
return generateFunc(extractedProp);
case PropTypesType.SHAPE:
return generateShape(type, extractedProp);
case PropTypesType.INSTANCEOF:
return createTypeDef({ name: PropTypesType.INSTANCEOF, summary: type.value });
case PropTypesType.OBJECTOF:
return generateObjectOf(type, extractedProp);
case PropTypesType.UNION:
return generateUnion(type, extractedProp);
case PropTypesType.ENUM:
return generateEnum(type);
case PropTypesType.ARRAYOF:
return generateArray(type, extractedProp);
default:
return createTypeDef({ name: type.name, summary: type.name });
}
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
}
return createTypeDef({ name: 'unknown', summary: 'unknown' });
}
export function createType(extractedProp: ExtractedProp): PropType {
const { type } = extractedProp.docgenInfo;
switch (type.name) {
case PropTypesType.CUSTOM:
case PropTypesType.SHAPE:
case PropTypesType.INSTANCEOF:
case PropTypesType.OBJECTOF:
case PropTypesType.UNION:
case PropTypesType.ENUM:
case PropTypesType.ARRAYOF: {
const { summary, detail } = generateType(type, extractedProp).value;
return {
summary,
detail: summary !== detail ? detail : undefined,
};
}
case PropTypesType.FUNC: {
const { detail } = generateType(type, extractedProp).value;
return { summary: detail };
}
default:
return null;
}
}

View File

@ -0,0 +1,25 @@
import { generate } from 'escodegen';
const BASIC_OPTIONS = {
format: {
indent: {
style: ' ',
},
semicolons: false,
},
};
const COMPACT_OPTIONS = {
...BASIC_OPTIONS,
format: {
newline: '',
},
};
const PRETTY_OPTIONS = {
...BASIC_OPTIONS,
};
export function generateCode(ast: any, compact = false): string {
return generate(ast, compact ? COMPACT_OPTIONS : PRETTY_OPTIONS);
}

View File

@ -0,0 +1,156 @@
import { generateFuncSignature } from './generateFuncSignature';
import { parseJsDoc } from '../../../lib/jsdocParser';
it('should return an empty string with there is no @params and @returns tags', () => {
const result = generateFuncSignature(null, null);
expect(result).toBe('');
});
it('should return a signature with a single arg when there is a @param tag with a name', () => {
const { params, returns } = parseJsDoc('@param event').extractedTags;
const result = generateFuncSignature(params, returns);
expect(result).toBe('(event)');
});
it('should return a signature with a single arg when there is a @param tag with a name and a type', () => {
const { params, returns } = parseJsDoc('@param {SyntheticEvent} event').extractedTags;
const result = generateFuncSignature(params, returns);
expect(result).toBe('(event: SyntheticEvent)');
});
it('should return a signature with a single arg when there is a @param tag with a name, a type and a desc', () => {
const { params, returns } = parseJsDoc(
'@param {SyntheticEvent} event - React event'
).extractedTags;
const result = generateFuncSignature(params, returns);
expect(result).toBe('(event: SyntheticEvent)');
});
it('should support @param of record type', () => {
const { params, returns } = parseJsDoc('@param {{a: number}} event').extractedTags;
const result = generateFuncSignature(params, returns);
expect(result).toBe('(event: ({a: number}))');
});
it('should support @param of union type', () => {
const { params, returns } = parseJsDoc('@param {(number|boolean)} event').extractedTags;
const result = generateFuncSignature(params, returns);
expect(result).toBe('(event: (number|boolean))');
});
it('should support @param of array type', () => {
const { params, returns } = parseJsDoc('@param {number[]} event').extractedTags;
const result = generateFuncSignature(params, returns);
expect(result).toBe('(event: number[])');
});
it('should support @param with a nullable type', () => {
const { params, returns } = parseJsDoc('@param {?number} event').extractedTags;
const result = generateFuncSignature(params, returns);
expect(result).toBe('(event: number)');
});
it('should support @param with a non nullable type', () => {
const { params, returns } = parseJsDoc('@param {!number} event').extractedTags;
const result = generateFuncSignature(params, returns);
expect(result).toBe('(event: number)');
});
it('should support optional @param with []', () => {
const { params, returns } = parseJsDoc('@param {number} [event]').extractedTags;
const result = generateFuncSignature(params, returns);
expect(result).toBe('(event: number)');
});
it('should support optional @param with =', () => {
const { params, returns } = parseJsDoc('@param {number=} event').extractedTags;
const result = generateFuncSignature(params, returns);
expect(result).toBe('(event: number)');
});
it('should support @param of type any', () => {
const { params, returns } = parseJsDoc('@param {*} event').extractedTags;
const result = generateFuncSignature(params, returns);
expect(result).toBe('(event: any)');
});
it('should support multiple @param tags', () => {
const { params, returns } = parseJsDoc(
'@param {SyntheticEvent} event\n@param {string} customData'
).extractedTags;
const result = generateFuncSignature(params, returns);
expect(result).toBe('(event: SyntheticEvent, customData: string)');
});
it('should return a signature with a return type when there is a @returns with a type', () => {
const { params, returns } = parseJsDoc('@returns {string}').extractedTags;
const result = generateFuncSignature(params, returns);
expect(result).toBe('() => string');
});
it('should support @returns of record type', () => {
const { params, returns } = parseJsDoc('@returns {{a: number, b: string}}').extractedTags;
const result = generateFuncSignature(params, returns);
expect(result).toBe('() => ({a: number, b: string})');
});
it('should support @returns of array type', () => {
const { params, returns } = parseJsDoc('@returns {integer[]}').extractedTags;
const result = generateFuncSignature(params, returns);
expect(result).toBe('() => integer[]');
});
it('should support @returns of union type', () => {
const { params, returns } = parseJsDoc('@returns {(number|boolean)}').extractedTags;
const result = generateFuncSignature(params, returns);
expect(result).toBe('() => (number|boolean)');
});
it('should support @returns type any', () => {
const { params, returns } = parseJsDoc('@returns {*}').extractedTags;
const result = generateFuncSignature(params, returns);
expect(result).toBe('() => any');
});
it('should support @returns of type void', () => {
const { params, returns } = parseJsDoc('@returns {void}').extractedTags;
const result = generateFuncSignature(params, returns);
expect(result).toBe('() => void');
});
it('should return a full signature when there is a single @param tag and a @returns', () => {
const { params, returns } = parseJsDoc(
'@param {SyntheticEvent} event - React event.\n@returns {string}'
).extractedTags;
const result = generateFuncSignature(params, returns);
expect(result).toBe('(event: SyntheticEvent) => string');
});
it('should return a full signature when there is a multiple @param tags and a @returns', () => {
const { params, returns } = parseJsDoc(
'@param {SyntheticEvent} event - React event.\n@param {string} data\n@returns {string}'
).extractedTags;
const result = generateFuncSignature(params, returns);
expect(result).toBe('(event: SyntheticEvent, data: string) => string');
});

View File

@ -0,0 +1,39 @@
import { isNil } from 'lodash';
import { ExtractedJsDocParam, ExtractedJsDocReturns } from '../../../lib/jsdocParser';
export function generateFuncSignature(
params: ExtractedJsDocParam[],
returns: ExtractedJsDocReturns
): string {
const hasParams = !isNil(params);
const hasReturns = !isNil(returns);
if (!hasParams && !hasReturns) {
return '';
}
const funcParts = [];
if (hasParams) {
const funcParams = params.map((x: ExtractedJsDocParam) => {
const prettyName = x.getPrettyName();
const typeName = x.getTypeName();
if (!isNil(typeName)) {
return `${prettyName}: ${typeName}`;
}
return prettyName;
});
funcParts.push(`(${funcParams.join(', ')})`);
} else {
funcParts.push('()');
}
if (hasReturns) {
funcParts.push(`=> ${returns.getTypeName()}`);
}
return funcParts.join(' ');
}

View File

@ -0,0 +1,955 @@
/* eslint-disable no-underscore-dangle */
import { PropDef } from '@storybook/components';
import PropTypes from 'prop-types';
import { Component } from '../../../blocks/shared';
import { extractPropsFromDocgen, DocgenInfo } from '../../../lib/docgen';
import { enhancePropTypesProp, enhancePropTypesProps } from './handleProp';
const DOCGEN_SECTION = 'props';
function createDocgenSection(docgenInfo: DocgenInfo): Record<string, any> {
return {
[DOCGEN_SECTION]: {
...docgenInfo,
},
};
}
function createDocgenProp({
name,
type,
...others
}: Partial<DocgenInfo> & { name: string }): Record<string, any> {
return {
[name]: {
type,
required: false,
...others,
},
};
}
// eslint-disable-next-line react/forbid-foreign-prop-types
function createComponent({ propTypes = {}, defaultProps = {}, docgenInfo = {} }): Component {
const component = () => {};
component.propTypes = propTypes;
component.defaultProps = defaultProps;
// @ts-ignore
component.__docgenInfo = createDocgenSection(docgenInfo);
return component;
}
function extractPropDef(component: Component): PropDef {
return enhancePropTypesProp(extractPropsFromDocgen(component, DOCGEN_SECTION)[0]);
}
describe('enhancePropTypesProp', () => {
describe('type', () => {
function createTestComponent(docgenInfo: Partial<DocgenInfo>): Component {
return createComponent({
docgenInfo: {
...createDocgenProp({ name: 'prop', ...docgenInfo }),
},
});
}
describe('custom', () => {
describe('when raw value is available', () => {
it('should support literal', () => {
const component = createTestComponent({
type: {
name: 'custom',
raw: 'MY_LITERAL',
},
});
const { type } = extractPropDef(component);
expect(type.summary).toBe('MY_LITERAL');
expect(type.detail).toBeUndefined();
});
it('should support short object', () => {
const component = createTestComponent({
type: {
name: 'custom',
raw: '{\n text: PropTypes.string.isRequired,\n}',
},
});
const { type } = extractPropDef(component);
const expectedSummary = '{ text: string }';
expect(type.summary.replace(/\s/g, '')).toBe(expectedSummary.replace(/\s/g, ''));
expect(type.detail).toBeUndefined();
});
it('should support long object', () => {
const component = createTestComponent({
type: {
name: 'custom',
raw:
'{\n text: PropTypes.string.isRequired,\n value: PropTypes.string.isRequired,\n}',
},
});
const { type } = extractPropDef(component);
expect(type.summary).toBe('object');
const expectedDetail = `{
text: string,
value: string
}`;
expect(type.detail.replace(/\s/g, '')).toBe(expectedDetail.replace(/\s/g, ''));
});
it('should use identifier of a React element when available', () => {
const component = createTestComponent({
type: {
name: 'custom',
raw:
'function InlinedFunctionalComponent() {\n return <div>Inlined FunctionnalComponent!</div>;\n}',
},
});
const { type } = extractPropDef(component);
expect(type.summary).toBe('InlinedFunctionalComponent');
const expectedDetail = `function InlinedFunctionalComponent() {
return <div>Inlined FunctionnalComponent!</div>;
}`;
expect(type.detail.replace(/\s/g, '')).toBe(expectedDetail.replace(/\s/g, ''));
});
it('should not use identifier of a HTML element', () => {
const component = createTestComponent({
type: {
name: 'custom',
raw: '<div>Hello world!</div>',
},
});
const { type } = extractPropDef(component);
expect(type.summary).toBe('element');
const expectedDetail = '<div>Hello world!</div>';
expect(type.detail.replace(/\s/g, '')).toBe(expectedDetail.replace(/\s/g, ''));
});
it('should support element without identifier', () => {
const component = createTestComponent({
type: {
name: 'custom',
raw: '() => {\n return <div>Inlined FunctionnalComponent!</div>;\n}',
},
});
const { type } = extractPropDef(component);
expect(type.summary).toBe('element');
const expectedDetail = `() => {
return <div>Inlined FunctionnalComponent!</div>;
}`;
expect(type.detail.replace(/\s/g, '')).toBe(expectedDetail.replace(/\s/g, ''));
});
it('should return "custom" when it is not a known type', () => {
const component = createTestComponent({
type: {
name: 'custom',
raw: 'Symbol("Hey!")',
},
});
const { type } = extractPropDef(component);
expect(type.summary).toBe('custom');
});
});
it("should return 'custom' when there is no raw value", () => {
const component = createTestComponent({
type: {
name: 'custom',
},
});
const { type } = extractPropDef(component);
expect(type.summary).toBe('custom');
});
});
[
'any',
'bool',
'string',
'number',
'symbol',
'object',
'element',
'elementType',
'node',
].forEach(x => {
it(`should return '${x}' when type is ${x}`, () => {
const component = createTestComponent({
type: {
name: x,
},
});
const { type } = extractPropDef(component);
expect(type.summary).toBe(x);
});
});
it('should support short shape', () => {
const component = createTestComponent({
type: {
name: 'shape',
value: {
foo: {
name: 'string',
required: false,
},
},
},
});
const { type } = extractPropDef(component);
const expectedSummary = '{ foo: string }';
expect(type.summary.replace(/\s/g, '')).toBe(expectedSummary.replace(/\s/g, ''));
expect(type.detail).toBeUndefined();
});
it('should support long shape', () => {
const component = createTestComponent({
type: {
name: 'shape',
value: {
foo: {
name: 'string',
required: false,
},
bar: {
name: 'string',
required: false,
},
another: {
name: 'string',
required: false,
},
},
},
});
const { type } = extractPropDef(component);
expect(type.summary).toBe('object');
const expectedDetail = `{
foo: string,
bar: string,
another: string
}`;
expect(type.detail.replace(/\s/g, '')).toBe(expectedDetail.replace(/\s/g, ''));
});
it('should support enum of string', () => {
const component = createTestComponent({
type: {
name: 'enum',
value: [
{
value: "'News'",
computed: false,
},
{
value: "'Photos'",
computed: false,
},
],
},
});
const { type } = extractPropDef(component);
expect(type.summary).toBe("'News' | 'Photos'");
});
it('should support enum of object', () => {
const component = createTestComponent({
type: {
name: 'enum',
value: [
{
value:
'{\n text: PropTypes.string.isRequired,\n value: PropTypes.string.isRequired,\n}',
computed: true,
},
{
value: '{\n foo: PropTypes.string,\n bar: PropTypes.string,\n}',
computed: true,
},
],
},
});
const { type } = extractPropDef(component);
expect(type.summary).toBe('object | object');
const expectedDetail = `{
text: string,
value: string
} | {
foo: string,
bar: string
}`;
expect(type.detail.replace(/\s/g, '')).toBe(expectedDetail.replace(/\s/g, ''));
});
it('should support enum of element', () => {
const component = createTestComponent({
type: {
name: 'enum',
value: [
{
value: '() => {\n return <div>FunctionnalComponent!</div>;\n}',
computed: true,
},
{
value:
'class ClassComponent extends React.PureComponent {\n render() {\n return <div>ClassComponent!</div>;\n }\n}',
computed: true,
},
],
},
});
const { type } = extractPropDef(component);
expect(type.summary).toBe('element | ClassComponent');
const expectedDetail = `() => {
return <div>FunctionnalComponent!</div>;
} | class ClassComponent extends React.PureComponent {
render() {
return <div>ClassComponent!</div>;
}
}`;
expect(type.detail.replace(/\s/g, '')).toBe(expectedDetail.replace(/\s/g, ''));
});
describe('func', () => {
it('should return "func" when the prop dont have a description', () => {
const component = createTestComponent({
type: {
name: 'func',
},
});
const { type } = extractPropDef(component);
expect(type.summary).toBe('func');
});
it('should return "func" when the prop have a description without JSDoc tags', () => {
const component = createTestComponent({
type: {
name: 'func',
},
description: 'Hey! Hey!',
});
const { type } = extractPropDef(component);
expect(type.summary).toBe('func');
});
it('should return a func signature when there is JSDoc tags.', () => {
const component = createTestComponent({
type: {
name: 'func',
},
description: '@param event\n@param data\n@returns {string}',
});
const { type } = extractPropDef(component);
expect(type.summary).toBe('(event, data) => string');
});
});
it('should return the instance type when type is instanceOf', () => {
const component = createTestComponent({
type: {
name: 'instanceOf',
value: 'Set',
},
});
const { type } = extractPropDef(component);
expect(type.summary).toBe('Set');
});
describe('objectOf', () => {
it('should support objectOf primitive', () => {
const component = createTestComponent({
type: {
name: 'objectOf',
value: {
name: 'number',
},
},
});
const { type } = extractPropDef(component);
expect(type.summary).toBe('objectOf(number)');
expect(type.detail).toBeUndefined();
});
it('should support objectOf of identifier', () => {
const component = createTestComponent({
type: {
name: 'objectOf',
value: {
name: 'custom',
raw: 'NAMED_OBJECT',
},
},
});
const { type } = extractPropDef(component);
expect(type.summary).toBe('objectOf(NAMED_OBJECT)');
expect(type.detail).toBeUndefined();
});
it('should support objectOf short object', () => {
const component = createTestComponent({
type: {
name: 'objectOf',
value: {
name: 'custom',
raw: '{\n foo: PropTypes.string,\n}',
},
},
});
const { type } = extractPropDef(component);
expect(type.summary).toBe('objectOf({ foo: string })');
expect(type.detail).toBeUndefined();
});
it('should support objectOf long object', () => {
const component = createTestComponent({
type: {
name: 'objectOf',
value: {
name: 'custom',
raw:
'{\n foo: PropTypes.string,\n bar: PropTypes.string,\n another: PropTypes.string,\n}',
},
},
});
const { type } = extractPropDef(component);
expect(type.summary).toBe('objectOf(object)');
const expectedDetail = `objectOf({
foo: string,
bar: string,
another: string
})`;
expect(type.detail.replace(/\s/g, '')).toBe(expectedDetail.replace(/\s/g, ''));
});
it('should support objectOf short shape', () => {
const component = createTestComponent({
type: {
name: 'objectOf',
value: {
name: 'shape',
value: {
foo: {
name: 'string',
required: false,
},
},
},
},
});
const { type } = extractPropDef(component);
expect(type.summary).toBe('objectOf({ foo: string })');
expect(type.detail).toBeUndefined();
});
it('should support objectOf long shape', () => {
const component = createTestComponent({
type: {
name: 'objectOf',
value: {
name: 'shape',
value: {
foo: {
name: 'string',
required: false,
},
bar: {
name: 'string',
required: false,
},
another: {
name: 'string',
required: false,
},
},
},
},
});
const { type } = extractPropDef(component);
expect(type.summary).toBe('objectOf(object)');
const expectedDetail = `objectOf({
foo: string,
bar: string,
another: string
})`;
expect(type.detail.replace(/\s/g, '')).toBe(expectedDetail.replace(/\s/g, ''));
});
});
it('should support union', () => {
const component = createTestComponent({
type: {
name: 'union',
value: [
{
name: 'string',
},
{
name: 'instanceOf',
value: 'Set',
},
],
},
});
const { type } = extractPropDef(component);
expect(type.summary).toBe('string | Set');
expect(type.detail).toBeUndefined();
});
describe('array', () => {
it('should support array of primitive', () => {
const component = createTestComponent({
type: {
name: 'arrayOf',
value: {
name: 'number',
},
},
});
const { type } = extractPropDef(component);
expect(type.summary).toBe('number[]');
expect(type.detail).toBeUndefined();
});
it('should support array of identifier', () => {
const component = createTestComponent({
type: {
name: 'arrayOf',
value: {
name: 'custom',
raw: 'NAMED_OBJECT',
},
},
});
const { type } = extractPropDef(component);
expect(type.summary).toBe('NAMED_OBJECT[]');
expect(type.detail).toBeUndefined();
});
it('should support array of short object', () => {
const component = createTestComponent({
type: {
name: 'arrayOf',
value: {
name: 'custom',
raw: '{\n foo: PropTypes.string,\n}',
},
},
});
const { type } = extractPropDef(component);
expect(type.summary).toBe('[{ foo: string }]');
expect(type.detail).toBeUndefined();
});
it('should support array of long object', () => {
const component = createTestComponent({
type: {
name: 'arrayOf',
value: {
name: 'custom',
raw:
'{\n text: PropTypes.string.isRequired,\n value: PropTypes.string.isRequired,\n}',
},
},
});
const { type } = extractPropDef(component);
expect(type.summary).toBe('object[]');
const expectedDetail = `[{
text: string,
value: string
}]`;
expect(type.detail.replace(/\s/g, '')).toBe(expectedDetail.replace(/\s/g, ''));
});
it('should support array of short shape', () => {
const component = createTestComponent({
type: {
name: 'arrayOf',
value: {
name: 'shape',
value: {
foo: {
name: 'string',
required: false,
},
},
},
},
});
const { type } = extractPropDef(component);
expect(type.summary).toBe('[{ foo: string }]');
expect(type.detail).toBeUndefined();
});
it('should support array of long shape', () => {
const component = createTestComponent({
type: {
name: 'arrayOf',
value: {
name: 'shape',
value: {
foo: {
name: 'string',
required: false,
},
bar: {
name: 'string',
required: false,
},
another: {
name: 'string',
required: false,
},
},
},
},
});
const { type } = extractPropDef(component);
expect(type.summary).toBe('object[]');
const expectedDetail = `[{
foo: string,
bar: string,
another: string
}]`;
expect(type.detail.replace(/\s/g, '')).toBe(expectedDetail.replace(/\s/g, ''));
});
});
});
describe('defaultValue', () => {
function createTestComponent(defaultValue: string): Component {
return createComponent({
docgenInfo: {
...createDocgenProp({
name: 'prop',
type: { name: 'custom' },
defaultValue: { value: defaultValue },
}),
},
});
}
it('should support short object', () => {
const component = createTestComponent("{ foo: 'foo', bar: 'bar' }");
const { defaultValue } = extractPropDef(component);
const expectedSummary = "{ foo: 'foo', bar: 'bar' }";
expect(defaultValue.summary.replace(/\s/g, '')).toBe(expectedSummary.replace(/\s/g, ''));
expect(defaultValue.detail).toBeUndefined();
});
it('should support long object', () => {
const component = createTestComponent("{ foo: 'foo', bar: 'bar', another: 'another' }");
const { defaultValue } = extractPropDef(component);
expect(defaultValue.summary).toBe('object');
const expectedDetail = `{
foo: 'foo',
bar: 'bar',
another: 'another'
}`;
expect(defaultValue.detail.replace(/\s/g, '')).toBe(expectedDetail.replace(/\s/g, ''));
});
it('should support short function', () => {
const component = createTestComponent('() => {}');
const { defaultValue } = extractPropDef(component);
expect(defaultValue.summary).toBe('() => {}');
expect(defaultValue.detail).toBeUndefined();
});
it('should support long function', () => {
const component = createTestComponent(
'(foo, bar) => {\n const concat = foo + bar;\n const append = concat + " hey!";\n \n return append;\n}'
);
const { defaultValue } = extractPropDef(component);
expect(defaultValue.summary).toBe('func');
const expectedDetail = `(foo, bar) => {
const concat = foo + bar;
const append = concat + ' hey!';
return append
}`;
expect(defaultValue.detail.replace(/\s/g, '')).toBe(expectedDetail.replace(/\s/g, ''));
});
it('should use the name of function when available and indicate that args are present', () => {
const component = createTestComponent('function concat(a, b) {\n return a + b;\n}');
const { defaultValue } = extractPropDef(component);
expect(defaultValue.summary).toBe('concat( ... )');
const expectedDetail = `function concat(a, b) {
return a + b
}`;
expect(defaultValue.detail.replace(/\s/g, '')).toBe(expectedDetail.replace(/\s/g, ''));
});
it('should use the name of function when available', () => {
const component = createTestComponent('function hello() {\n return "hello";\n}');
const { defaultValue } = extractPropDef(component);
expect(defaultValue.summary).toBe('hello()');
const expectedDetail = `function hello() {
return 'hello'
}`;
expect(defaultValue.detail.replace(/\s/g, '')).toBe(expectedDetail.replace(/\s/g, ''));
});
it('should support short element', () => {
const component = createTestComponent('<div>Hey!</div>');
const { defaultValue } = extractPropDef(component);
expect(defaultValue.summary).toBe('<div>Hey!</div>');
expect(defaultValue.detail).toBeUndefined();
});
it('should support long element', () => {
const component = createTestComponent(
'() => {\n return <div>Inlined FunctionnalComponent!</div>;\n}'
);
const { defaultValue } = extractPropDef(component);
expect(defaultValue.summary).toBe('element');
const expectedDetail = `() => {
return <div>Inlined FunctionnalComponent!</div>;
}`;
expect(defaultValue.detail.replace(/\s/g, '')).toBe(expectedDetail.replace(/\s/g, ''));
});
it("should use the name of the React component when it's available", () => {
const component = createTestComponent(
'function InlinedFunctionalComponent() {\n return <div>Inlined FunctionnalComponent!</div>;\n}'
);
const { defaultValue } = extractPropDef(component);
expect(defaultValue.summary).toBe('<InlinedFunctionalComponent />');
const expectedDetail = `function InlinedFunctionalComponent() {
return <div>Inlined FunctionnalComponent!</div>;
}`;
expect(defaultValue.detail.replace(/\s/g, '')).toBe(expectedDetail.replace(/\s/g, ''));
});
it('should not use the name of an HTML element', () => {
const component = createTestComponent('<div>Hey!</div>');
const { defaultValue } = extractPropDef(component);
expect(defaultValue.summary).not.toBe('<div />');
});
it('should support short array', () => {
const component = createTestComponent('[1]');
const { defaultValue } = extractPropDef(component);
expect(defaultValue.summary).toBe('[1]');
expect(defaultValue.detail).toBeUndefined();
});
it('should support long array', () => {
const component = createTestComponent(
'[\n {\n thing: {\n id: 2,\n func: () => {},\n arr: [],\n },\n },\n]'
);
const { defaultValue } = extractPropDef(component);
expect(defaultValue.summary).toBe('array');
const expectedDetail = `[{
thing: {
id: 2,
func: () => {
},
arr: []
}
}]`;
expect(defaultValue.detail.replace(/\s/g, '')).toBe(expectedDetail.replace(/\s/g, ''));
});
});
});
describe('enhancePropTypesProps', () => {
it('should keep the original definition order', () => {
const component = createComponent({
propTypes: {
foo: PropTypes.string,
middleWithDefaultValue: PropTypes.string,
bar: PropTypes.string,
endWithDefaultValue: PropTypes.string,
},
docgenInfo: {
...createDocgenProp({
name: 'middleWithDefaultValue',
type: { name: 'string' },
defaultValue: { value: 'Middle!' },
}),
...createDocgenProp({
name: 'endWithDefaultValue',
type: { name: 'string' },
defaultValue: { value: 'End!' },
}),
...createDocgenProp({
name: 'foo',
type: { name: 'string' },
}),
...createDocgenProp({
name: 'bar',
type: { name: 'string' },
}),
},
});
const props = enhancePropTypesProps(
extractPropsFromDocgen(component, DOCGEN_SECTION),
component
);
expect(props.length).toBe(4);
expect(props[0].name).toBe('foo');
expect(props[1].name).toBe('middleWithDefaultValue');
expect(props[2].name).toBe('bar');
expect(props[3].name).toBe('endWithDefaultValue');
});
it('should not include @ignore props', () => {
const component = createComponent({
propTypes: {
foo: PropTypes.string,
bar: PropTypes.string,
},
docgenInfo: {
...createDocgenProp({
name: 'foo',
type: { name: 'string' },
}),
...createDocgenProp({
name: 'bar',
type: { name: 'string' },
description: '@ignore',
}),
},
});
const props = enhancePropTypesProps(
extractPropsFromDocgen(component, DOCGEN_SECTION),
component
);
expect(props.length).toBe(1);
expect(props[0].name).toBe('foo');
});
});

View File

@ -0,0 +1,35 @@
import { isNil } from 'lodash';
import { PropDef } from '@storybook/components';
import { ExtractedProp } from '../../../lib/docgen';
import { createType } from './createType';
import { createDefaultValue } from './createDefaultValue';
import { Component } from '../../../blocks/shared';
import { keepOriginalDefinitionOrder } from './sortProps';
export function enhancePropTypesProp(extractedProp: ExtractedProp): PropDef {
const { propDef } = extractedProp;
const newtype = createType(extractedProp);
if (!isNil(newtype)) {
propDef.type = newtype;
}
const { defaultValue } = extractedProp.docgenInfo;
if (!isNil(defaultValue)) {
const newDefaultValue = createDefaultValue(defaultValue.value);
if (!isNil(newDefaultValue)) {
propDef.defaultValue = newDefaultValue;
}
}
return propDef;
}
export function enhancePropTypesProps(
extractedProps: ExtractedProp[],
component: Component
): PropDef[] {
const enhancedProps = extractedProps.map(enhancePropTypesProp);
return keepOriginalDefinitionOrder(enhancedProps, component);
}

View File

@ -0,0 +1,5 @@
import htmlTags from 'html-tags';
export function isHtmlTag(tagName: string): boolean {
return htmlTags.includes(tagName.toLowerCase());
}

View File

@ -0,0 +1,21 @@
import { PropDef } from '@storybook/components';
import { isNil } from 'lodash';
import { Component } from '../../../blocks/shared';
// react-docgen doesn't returned the props in the order they were defined in the "propTypes" object of the component.
// This function re-order them by their original definition order.
export function keepOriginalDefinitionOrder(
extractedProps: PropDef[],
component: Component
): PropDef[] {
// eslint-disable-next-line react/forbid-foreign-prop-types
const { propTypes } = component;
if (!isNil(propTypes)) {
return Object.keys(propTypes)
.map(x => extractedProps.find(y => y.name === x))
.filter(x => x);
}
return extractedProps;
}

View File

@ -4,7 +4,7 @@ import toReact from '@egoist/vue-to-react';
import { StoryFn } from '@storybook/addons';
import { addParameters } from '@storybook/client-api';
import { extractProps } from './extractProps';
import { extractComponentDescription } from '../../lib/docgenUtils';
import { extractComponentDescription } from '../../lib/docgen/utils';
addParameters({
docs: {

View File

@ -1,5 +1,5 @@
import { PropDef } from '@storybook/components';
import { PropsExtractor, extractPropsFromDocgen, hasDocgen } from '../../lib/docgenUtils';
import { PropsExtractor, hasDocgen, extractPropsFromDocgen } from '../../lib/docgen';
const SECTIONS = ['props', 'events', 'slots'];
@ -9,7 +9,7 @@ export const extractProps: PropsExtractor = component => {
}
const sections: Record<string, PropDef[]> = {};
SECTIONS.forEach(section => {
sections[section] = extractPropsFromDocgen(component, section);
sections[section] = extractPropsFromDocgen(component, section).map(x => x.propDef);
});
return { sections };
};

View File

@ -0,0 +1,8 @@
export function webpack(webpackConfig: any = {}, options: any = {}) {
webpackConfig.module.rules.push({
test: /\.vue$/,
loader: 'vue-docgen-loader',
enforce: 'post',
});
return webpackConfig;
}

View File

@ -8,10 +8,10 @@ import { render } from 'lit-html';
function mapData(data) {
return data.map(item => ({
name: item.name,
type: { name: item.type },
type: { summary: item.type },
required: '',
description: item.description,
defaultValue: item.default,
defaultValue: { summary: item.default },
}));
}

View File

@ -1,16 +0,0 @@
export interface DocgenInfo {
type?: {
name: string;
value?: {
name?: string;
raw?: string;
};
};
flowType?: any;
tsType?: any;
required: boolean;
description?: string;
defaultValue?: {
value: string;
};
}

View File

@ -0,0 +1,23 @@
import { isNil } from 'lodash';
import { PropDefaultValue } from '@storybook/components';
import { DocgenPropDefaultValue } from './types';
const BLACKLIST = ['null', 'undefined'];
function isDefaultValueBlacklisted(value: string) {
return BLACKLIST.some(x => x === value);
}
export function createDefaultValue(defaultValue: DocgenPropDefaultValue): PropDefaultValue {
if (!isNil(defaultValue)) {
const { value } = defaultValue;
if (!isDefaultValueBlacklisted(value)) {
return {
summary: value,
};
}
}
return null;
}

View File

@ -0,0 +1,99 @@
import { isNil } from 'lodash';
import { PropDef } from '@storybook/components';
import { TypeSystem, DocgenInfo, DocgenType } from './types';
import { JsDocParsingResult } from '../jsdocParser';
import { createDefaultValue } from './createDefaultValue';
export type PropDefFactory = (
propName: string,
docgenInfo: DocgenInfo,
jsDocParsingResult?: JsDocParsingResult
) => PropDef;
function createBasicPropDef(name: string, type: DocgenType, docgenInfo: DocgenInfo): PropDef {
const { description, required, defaultValue } = docgenInfo;
return {
name,
type: { summary: type.name },
required,
description,
defaultValue: createDefaultValue(defaultValue),
};
}
function createPropDef(
name: string,
type: DocgenType,
docgenInfo: DocgenInfo,
jsDocParsingResult: JsDocParsingResult
): PropDef {
const propDef = createBasicPropDef(name, type, docgenInfo);
if (jsDocParsingResult.includesJsDoc) {
const { description, extractedTags } = jsDocParsingResult;
if (!isNil(description)) {
propDef.description = jsDocParsingResult.description;
}
const hasParams = !isNil(extractedTags.params);
const hasReturns = !isNil(extractedTags.returns) && !isNil(extractedTags.returns.type);
if (hasParams || hasReturns) {
propDef.jsDocTags = {
params:
hasParams &&
extractedTags.params.map(x => ({ name: x.getPrettyName(), description: x.description })),
returns: hasReturns && { description: extractedTags.returns.description },
};
}
}
return propDef;
}
export const javaScriptFactory: PropDefFactory = (
propName: string,
docgenInfo: DocgenInfo,
jsDocParsingResult?: JsDocParsingResult
) => {
return createPropDef(propName, docgenInfo.type, docgenInfo, jsDocParsingResult);
};
export const tsFactory: PropDefFactory = (
propName: string,
docgenInfo: DocgenInfo,
jsDocParsingResult?: JsDocParsingResult
) => {
return createPropDef(propName, docgenInfo.tsType, docgenInfo, jsDocParsingResult);
};
export const flowFactory: PropDefFactory = (
propName: string,
docgenInfo: DocgenInfo,
jsDocParsingResult?: JsDocParsingResult
) => {
return createPropDef(propName, docgenInfo.flowType, docgenInfo, jsDocParsingResult);
};
export const unknownFactory: PropDefFactory = (
propName: string,
docgenInfo: DocgenInfo,
jsDocParsingResult?: JsDocParsingResult
) => {
return createPropDef(propName, { name: 'unknown' }, docgenInfo, jsDocParsingResult);
};
export const getPropDefFactory = (typeSystem: TypeSystem): PropDefFactory => {
switch (typeSystem) {
case TypeSystem.JAVASCRIPT:
return javaScriptFactory;
case TypeSystem.TYPESCRIPT:
return tsFactory;
case TypeSystem.FLOW:
return flowFactory;
default:
return unknownFactory;
}
};

View File

@ -0,0 +1,155 @@
/* eslint-disable no-underscore-dangle */
import { Component } from '../../blocks/shared';
import { extractPropsFromDocgen } from './extractDocgenProps';
const DOCGEN_SECTION = 'props';
const PROP_NAME = 'propName';
interface TypeSystemDef {
name: string;
typeProperty?: string;
}
const TypeSystems: TypeSystemDef[] = [
{ name: 'javascript', typeProperty: 'type' },
{ name: 'typescript', typeProperty: 'tsType' },
];
function createType(typeName: string, others: Record<string, any> = {}): Record<string, string> {
return {
name: typeName,
...others,
};
}
function createStringType(typeSystemDef: TypeSystemDef, others: Record<string, any> = {}): any {
return {
[typeSystemDef.typeProperty]: createType('string', others),
};
}
function createFuncType(typeSystemDef: TypeSystemDef, others: Record<string, any> = {}): any {
const typeName = typeSystemDef.name === 'javascript' ? 'func' : '() => {}';
return {
[typeSystemDef.typeProperty]: createType(typeName, others),
};
}
function createComponent(docgenInfo: Record<string, any>): Component {
const component = () => {};
// @ts-ignore
component.__docgenInfo = {
[DOCGEN_SECTION]: {
[PROP_NAME]: {
required: false,
...docgenInfo,
},
},
};
return component;
}
TypeSystems.forEach(x => {
it('should map defaults docgen info properly', () => {
const component = createComponent({
...createStringType(x),
description: 'Hey! Hey!',
defaultValue: {
value: 'Default',
},
});
const { propDef } = extractPropsFromDocgen(component, DOCGEN_SECTION)[0];
expect(propDef.name).toBe(PROP_NAME);
expect(propDef.type.summary).toBe('string');
expect(propDef.description).toBe('Hey! Hey!');
expect(propDef.required).toBe(false);
expect(propDef.defaultValue.summary).toBe('Default');
});
it('should remove JSDoc tags from the description', () => {
const component = createComponent({
...createStringType(x),
description: 'Hey!\n@param event\nreturns {string}',
});
const { propDef } = extractPropsFromDocgen(component, DOCGEN_SECTION)[0];
expect(propDef.description).toBe('Hey!');
});
it('should not remove newline characters of multilines description without JSDoc tags', () => {
const component = createComponent({
...createStringType(x),
description: 'onClick description\nis a\nmulti-lines\ndescription',
});
const { propDef } = extractPropsFromDocgen(component, DOCGEN_SECTION)[0];
expect(propDef.description).toBe('onClick description\nis a\nmulti-lines\ndescription');
});
it('should not remove newline characters of multilines description with JSDoc tags', () => {
const component = createComponent({
...createFuncType(x),
description: 'onClick description\nis a\nmulti-lines\ndescription\n@param event',
});
const { propDef } = extractPropsFromDocgen(component, DOCGEN_SECTION)[0];
expect(propDef.description).toBe('onClick description\nis a\nmulti-lines\ndescription');
});
it('should not remove markdown from description without JSDoc tags', () => {
const component = createComponent({
...createStringType(x),
description: 'onClick *emphasis*, **strong**, `formatted` description.',
});
const { propDef } = extractPropsFromDocgen(component, DOCGEN_SECTION)[0];
expect(propDef.description).toBe('onClick *emphasis*, **strong**, `formatted` description.');
});
it('should not remove markdown from description with JSDoc tags', () => {
const component = createComponent({
...createFuncType(x),
description: 'onClick *emphasis*, **strong**, `formatted` description.\n@param event',
});
const { propDef } = extractPropsFromDocgen(component, DOCGEN_SECTION)[0];
expect(propDef.description).toBe('onClick *emphasis*, **strong**, `formatted` description.');
});
it('should return null when the property is marked with @ignore', () => {
const component = createComponent({
...createStringType(x),
description: 'onClick description\n@ignore',
});
expect(extractPropsFromDocgen(component, DOCGEN_SECTION).length).toBe(0);
});
it('should provide raw @param tags', () => {
const component = createComponent({
...createFuncType(x),
description:
'onClick description\n@param {SyntheticEvent} event - Original event.\n@param {string} value',
});
const { propDef } = extractPropsFromDocgen(component, DOCGEN_SECTION)[0];
expect(propDef.description).toBe('onClick description');
expect(propDef.jsDocTags).toBeDefined();
expect(propDef.jsDocTags.params).toBeDefined();
expect(propDef.jsDocTags.params[0].name).toBe('event');
expect(propDef.jsDocTags.params[0].description).toBe('Original event.');
expect(propDef.jsDocTags.params[1].name).toBe('value');
expect(propDef.jsDocTags.params[1].description).toBeNull();
});
});

View File

@ -0,0 +1,77 @@
import { isNil } from 'lodash';
import { PropDef } from '@storybook/components';
import { Component } from '../../blocks/shared';
import { ExtractedJsDoc, parseJsDoc } from '../jsdocParser';
import { DocgenInfo, TypeSystem } from './types';
import { getDocgenSection, isValidDocgenSection } from './utils';
import { getPropDefFactory, PropDefFactory } from './createPropDef';
export interface ExtractedProp {
propDef: PropDef;
docgenInfo: DocgenInfo;
jsDocTags: ExtractedJsDoc;
typeSystem: TypeSystem;
}
export type ExtractProps = (component: Component, section: string) => ExtractedProp[];
const getTypeSystem = (docgenInfo: DocgenInfo): TypeSystem => {
if (!isNil(docgenInfo.type)) {
return TypeSystem.JAVASCRIPT;
}
if (!isNil(docgenInfo.flowType)) {
return TypeSystem.FLOW;
}
if (!isNil(docgenInfo.tsType)) {
return TypeSystem.TYPESCRIPT;
}
return TypeSystem.UNKNOWN;
};
export const extractPropsFromDocgen: ExtractProps = (component, section) => {
const docgenSection = getDocgenSection(component, section);
if (!isValidDocgenSection(docgenSection)) {
return [];
}
const docgenPropsKeys = Object.keys(docgenSection);
const typeSystem = getTypeSystem(docgenSection[docgenPropsKeys[0]]);
const createPropDef = getPropDefFactory(typeSystem);
return docgenPropsKeys
.map(propName => {
const docgenInfo = docgenSection[propName];
return !isNil(docgenInfo)
? extractProp(propName, docgenInfo, typeSystem, createPropDef)
: null;
})
.filter(x => x);
};
function extractProp(
propName: string,
docgenInfo: DocgenInfo,
typeSystem: TypeSystem,
createPropDef: PropDefFactory
): ExtractedProp {
const jsDocParsingResult = parseJsDoc(docgenInfo.description);
const isIgnored = jsDocParsingResult.includesJsDoc && jsDocParsingResult.ignore;
if (!isIgnored) {
const propDef = createPropDef(propName, docgenInfo, jsDocParsingResult);
return {
propDef,
jsDocTags: jsDocParsingResult.extractedTags,
docgenInfo,
typeSystem,
};
}
return null;
}

View File

@ -0,0 +1,3 @@
export * from './types';
export * from './utils';
export * from './extractDocgenProps';

View File

@ -0,0 +1,44 @@
import { PropsTableProps } from '@storybook/components';
import { Component } from '../../blocks/shared';
export type PropsExtractor = (component: Component) => PropsTableProps | null;
export interface DocgenBaseType {
name: string;
description?: string;
require?: boolean;
}
export interface DocgenPropType extends DocgenBaseType {
value?: any;
raw?: string;
computed?: boolean;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface DocgenFlowType extends DocgenBaseType {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface DocgenTypeScriptType extends DocgenBaseType {}
export type DocgenType = DocgenPropType | DocgenFlowType | DocgenTypeScriptType;
export interface DocgenPropDefaultValue {
value: string;
}
export interface DocgenInfo {
type?: DocgenPropType;
flowType?: DocgenFlowType;
tsType?: DocgenTypeScriptType;
required: boolean;
description?: string;
defaultValue?: DocgenPropDefaultValue;
}
export enum TypeSystem {
JAVASCRIPT = 'JavaScript',
FLOW = 'Flow',
TYPESCRIPT = 'TypeScript',
UNKNOWN = 'Unknown',
}

View File

@ -0,0 +1,29 @@
/* eslint-disable no-underscore-dangle */
import { isNil } from 'lodash';
import { Component } from '../../blocks/shared';
export const str = (obj: any) => {
if (!obj) {
return '';
}
if (typeof obj === 'string') {
return obj as string;
}
throw new Error(`Description: expected string, got: ${JSON.stringify(obj)}`);
};
export function hasDocgen(component: Component): boolean {
return !!component.__docgenInfo;
}
export function isValidDocgenSection(docgenSection: any) {
return !isNil(docgenSection) && Object.keys(docgenSection).length > 0;
}
export function getDocgenSection(component: Component, section: string): any {
return hasDocgen(component) ? component.__docgenInfo[section] : null;
}
export const extractComponentDescription = (component?: Component) =>
component && hasDocgen(component) && str(component.__docgenInfo.description);

View File

@ -1,55 +0,0 @@
/* eslint-disable no-underscore-dangle */
import { PropDef, PropsTableProps } from '@storybook/components';
import { Component } from '../blocks/shared';
import { getTypeSystemHandler, getPropTypeSystem } from './type-system-handlers';
export type PropsExtractor = (component: Component) => PropsTableProps | null;
export type PropDefGetter = (component: Component, section: string) => PropDef[];
export const str = (o: any) => {
if (!o) {
return '';
}
if (typeof o === 'string') {
return o as string;
}
throw new Error(`Description: expected string, got: ${JSON.stringify(o)}`);
};
export const hasDocgen = (component: Component) => !!component.__docgenInfo;
export const hasDocgenSection = (component: Component, section: string) =>
component &&
component.__docgenInfo &&
component.__docgenInfo[section] &&
Object.keys(component.__docgenInfo[section]).length > 0;
export const extractPropsFromDocgen: PropDefGetter = (component, section) => {
if (!hasDocgenSection(component, section)) {
return [];
}
const props: Record<string, PropDef> = {};
const docgenInfoProps = component.__docgenInfo[section];
const propKeys = Object.keys(docgenInfoProps);
// Assuming the props for a given component will all have the same type system.
const typeSystem = getPropTypeSystem(docgenInfoProps[propKeys[0]]);
const typeSystemHandler = getTypeSystemHandler(typeSystem);
propKeys.forEach(propKey => {
const docgenInfoProp = docgenInfoProps[propKey];
const result = typeSystemHandler(propKey, docgenInfoProp);
if (!result.ignore) {
props[propKey] = result.propDef;
}
});
return Object.values(props);
};
export const extractComponentDescription = (component?: Component) =>
component && component.__docgenInfo && str(component.__docgenInfo.description);

View File

@ -1,187 +0,0 @@
import doctrine, { Annotation } from 'doctrine';
import { isNil } from 'lodash';
import { DocgenInfo } from './DocgenInfo';
export type ParseJsDoc = (docgenInfo: DocgenInfo) => JsDocParsingResult;
export interface JsDocParsingResult {
ignore: boolean;
description?: string;
extractedTags?: ExtractedJsDocTags;
}
export interface ExtractedJsDocParamTag {
name: string;
type?: doctrine.Type;
description?: string;
raw: doctrine.Tag;
getPrettyName: () => string;
getTypeName: () => string;
}
export interface ExtractedJsDocReturnsTag {
type?: doctrine.Type;
description?: string;
raw: doctrine.Tag;
getTypeName: () => string;
}
export interface ExtractedJsDocTags {
params?: ExtractedJsDocParamTag[];
returns?: ExtractedJsDocReturnsTag;
ignore: boolean;
}
function parse(content: string): Annotation {
let ast;
try {
ast = doctrine.parse(content, {
tags: ['param', 'arg', 'argument', 'returns', 'ignore'],
sloppy: true,
});
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
throw new Error('Cannot parse JSDoc tags.');
}
return ast;
}
export const parseJsDoc: ParseJsDoc = (docgenInfo: DocgenInfo) => {
const jsDocAst = parse(docgenInfo.description);
const extractedTags = extractJsDocTags(jsDocAst);
if (extractedTags.ignore) {
// There is no point in doing other stuff since this prop will not be rendered.
return {
ignore: true,
};
}
return {
ignore: false,
// Always use the parsed description to ensure JSDoc is removed from the description.
description: jsDocAst.description,
extractedTags,
};
};
function extractJsDocTags(ast: doctrine.Annotation): ExtractedJsDocTags {
const extractedTags: ExtractedJsDocTags = {
params: null,
returns: null,
ignore: false,
};
for (let i = 0; i < ast.tags.length; i += 1) {
const tag = ast.tags[i];
// arg & argument are aliases for param.
if (tag.title === 'param' || tag.title === 'arg' || tag.title === 'argument') {
const paramName = tag.name;
// When the @param doesn't have a name but have a type and a description, "null-null" is returned.
if (!isNil(paramName) && paramName !== 'null-null') {
if (isNil(extractedTags.params)) {
extractedTags.params = [];
}
extractedTags.params.push({
name: tag.name,
type: tag.type,
description: tag.description,
raw: tag,
getPrettyName: () => {
if (paramName.includes('null')) {
// There is a few cases in which the returned param name contains "null".
// - @param {SyntheticEvent} event- Original SyntheticEvent
// - @param {SyntheticEvent} event.\n@returns {string}
return paramName.replace('-null', '').replace('.null', '');
}
return tag.name;
},
getTypeName: () => {
return !isNil(tag.type) ? extractJsDocTypeName(tag.type) : null;
},
});
}
} else if (tag.title === 'returns') {
if (!isNil(tag.type)) {
extractedTags.returns = {
type: tag.type,
description: tag.description,
raw: tag,
getTypeName: () => {
return extractJsDocTypeName(tag.type);
},
};
}
} else if (tag.title === 'ignore') {
extractedTags.ignore = true;
// Once we reach an @ignore tag, there is no point in parsing the other tags since we will not render the prop.
break;
}
}
return extractedTags;
}
// FIXME: type argument should be doctrine.Type instead of any.
function extractJsDocTypeName(type: any): string {
if (type.type === 'NameExpression') {
return type.name;
}
if (type.type === 'RecordType') {
const recordFields = type.fields.map((field: doctrine.type.FieldType) => {
if (!isNil(field.value)) {
const valueTypeName = extractJsDocTypeName(field.value);
return `${field.key}: ${valueTypeName}`;
}
return field.key;
});
return `({${recordFields.join(', ')}})`;
}
if (type.type === 'UnionType') {
const unionElements = type.elements.map(extractJsDocTypeName);
return `(${unionElements.join('|')})`;
}
// Only support untyped array: []. Might add more support later if required.
if (type.type === 'ArrayType') {
return '[]';
}
if (type.type === 'TypeApplication') {
if (!isNil(type.expression)) {
if (type.expression.name === 'Array') {
const arrayType = extractJsDocTypeName(type.applications[0]);
return `${arrayType}[]`;
}
}
}
if (
type.type === 'NullableType' ||
type.type === 'NonNullableType' ||
type.type === 'OptionalType'
) {
return extractJsDocTypeName(type.expression);
}
if (type.type === 'AllLiteral') {
return 'any';
}
return null;
}

View File

@ -0,0 +1,344 @@
import { parseJsDoc } from './jsdocParser';
describe('parseJsDoc', () => {
it('should set includesJsDoc to false when the value is null', () => {
const { includesJsDoc, description, extractedTags } = parseJsDoc(null);
expect(includesJsDoc).toBeFalsy();
expect(description).toBeUndefined();
expect(extractedTags).toBeUndefined();
});
it('should set includesJsDocto to false when the value dont contains JSDoc', () => {
const { includesJsDoc, description, extractedTags } = parseJsDoc('Hey!');
expect(includesJsDoc).toBeFalsy();
expect(description).toBeUndefined();
expect(extractedTags).toBeUndefined();
});
it('should set includesJsDoc to true when the value contains JSDoc', () => {
const { includesJsDoc } = parseJsDoc('Hey!\n@version 1.2');
expect(includesJsDoc).toBeTruthy();
});
it('should remove all JSDoc tags from the description', () => {
const { description } = parseJsDoc('Hey!\n@version 1.2\n@deprecated');
expect(description).toBe('Hey!');
});
describe('@ignore', () => {
it('should set ignore to true when @ignore is present', () => {
const { ignore, description, extractedTags } = parseJsDoc('Hey!\n@ignore');
expect(ignore).toBeTruthy();
expect(description).toBeUndefined();
expect(extractedTags).toBeUndefined();
});
it('should set ignore to false when @ignore is not present', () => {
const { ignore } = parseJsDoc('Hey!\n@version 1.2');
expect(ignore).toBeFalsy();
});
});
describe('@param', () => {
it('should ignore invalid @param tags', () => {
const { extractedTags } = parseJsDoc('@param');
expect(extractedTags.params).toBeNull();
});
it('should return a @param with a name', () => {
const { extractedTags } = parseJsDoc('@param event');
expect(extractedTags.params).not.toBeNull();
expect(extractedTags.params[0].name).toBe('event');
expect(extractedTags.params[0].type).toBeNull();
expect(extractedTags.params[0].description).toBeNull();
});
it('should return a @param with a name and a type', () => {
const { extractedTags } = parseJsDoc('@param {SyntheticEvent} event');
expect(extractedTags.params).not.toBeNull();
expect(extractedTags.params[0].name).toBe('event');
expect(extractedTags.params[0].type).not.toBeNull();
expect(extractedTags.params[0].type.name).toBe('SyntheticEvent');
expect(extractedTags.params[0].description).toBeNull();
});
it('should return a @param with a name, a type and a description', () => {
const { extractedTags } = parseJsDoc('@param {SyntheticEvent} event - React event');
expect(extractedTags.params).not.toBeNull();
expect(extractedTags.params[0].name).toBe('event');
expect(extractedTags.params[0].type).not.toBeNull();
expect(extractedTags.params[0].type.name).toBe('SyntheticEvent');
expect(extractedTags.params[0].description).toBe('React event');
});
it('should support multiple @param tags', () => {
const { extractedTags } = parseJsDoc(
'@param {SyntheticEvent} event1 - React event\n@param {SyntheticEvent} event2 - React event\n@param {SyntheticEvent} event3 - React event'
);
['event1', 'event2', 'event3'].forEach((x, i) => {
expect(extractedTags.params[i].name).toBe(x);
expect(extractedTags.params[i].type).not.toBeNull();
expect(extractedTags.params[i].type.name).toBe('SyntheticEvent');
expect(extractedTags.params[i].description).toBe('React event');
});
});
it('should not return extra @param', () => {
const { extractedTags } = parseJsDoc('@param event');
expect(Object.keys(extractedTags.params).length).toBe(1);
});
it('should support multiline description when there is a @param', () => {
const { description, extractedTags } = parseJsDoc(
'This is a\nmultiline description\n@param event'
);
expect(description).toBe('This is a\nmultiline description');
expect(extractedTags.params).not.toBeNull();
expect(extractedTags.params[0].name).toBe('event');
});
it('should support multiline @param description', () => {
const { extractedTags } = parseJsDoc(
'@param event - This is a\nmultiline description\n@param anotherEvent'
);
expect(extractedTags.params).not.toBeNull();
expect(extractedTags.params[0].name).toBe('event');
expect(extractedTags.params[0].description).toBe('This is a\nmultiline description');
expect(extractedTags.params[1].name).toBe('anotherEvent');
});
['@arg', '@argument'].forEach(x => {
it(`should support ${x} alias`, () => {
const { extractedTags } = parseJsDoc(`${x} {SyntheticEvent} event - React event`);
expect(extractedTags.params).not.toBeNull();
expect(extractedTags.params[0].name).toBe('event');
expect(extractedTags.params[0].type).not.toBeNull();
expect(extractedTags.params[0].type.name).toBe('SyntheticEvent');
expect(extractedTags.params[0].description).toBe('React event');
});
});
describe('getTypeName', () => {
it('should support record type with a single field', () => {
const { extractedTags } = parseJsDoc('@param {{a: number}} event');
expect(extractedTags.params[0].getTypeName()).toBe('({a: number})');
});
it('should support record type with multiple fields', () => {
const { extractedTags } = parseJsDoc('@param {{a: number, b: string}} event');
expect(extractedTags.params[0].getTypeName()).toBe('({a: number, b: string})');
});
it('should support record type with a field having only a name', () => {
const { extractedTags } = parseJsDoc('@param {{a}} event');
expect(extractedTags.params[0].getTypeName()).toBe('({a})');
});
it('should support union type', () => {
const { extractedTags } = parseJsDoc('@param {(number|boolean)} event');
expect(extractedTags.params[0].getTypeName()).toBe('(number|boolean)');
});
it('should support array type', () => {
const { extractedTags } = parseJsDoc('@param {number[]} event');
expect(extractedTags.params[0].getTypeName()).toBe('number[]');
});
it('should support untyped array type', () => {
const { extractedTags } = parseJsDoc('@param {[]} event');
expect(extractedTags.params[0].getTypeName()).toBe('[]');
});
it('should support nullable type', () => {
const { extractedTags } = parseJsDoc('@param {?number} event');
expect(extractedTags.params[0].getTypeName()).toBe('number');
});
it('should support non nullable type', () => {
const { extractedTags } = parseJsDoc('@param {!number} event');
expect(extractedTags.params[0].getTypeName()).toBe('number');
});
it('should support optional param with []', () => {
const { extractedTags } = parseJsDoc('@param {number} [event]');
expect(extractedTags.params[0].getTypeName()).toBe('number');
});
it('should support optional param with =', () => {
const { extractedTags } = parseJsDoc('@param {number=} event');
expect(extractedTags.params[0].getTypeName()).toBe('number');
});
it('should support any type', () => {
const { extractedTags } = parseJsDoc('@param {*} event');
expect(extractedTags.params[0].getTypeName()).toBe('any');
});
});
describe('getPrettyName', () => {
it('should return @param name', () => {
const { extractedTags } = parseJsDoc('@param {SyntheticEvent} event - React event');
expect(extractedTags.params[0].getPrettyName()).toBe('event');
});
it('should fix missing space between the @param name and the description separator', () => {
const { extractedTags } = parseJsDoc('@param {SyntheticEvent} event- React event');
expect(extractedTags.params[0].getPrettyName()).toBe('event');
});
it('should fix @param name ending with . followed by a @returns tag', () => {
const { extractedTags } = parseJsDoc('@param {SyntheticEvent} event.\n');
expect(extractedTags.params[0].getPrettyName()).toBe('event');
});
});
});
describe('@returns', () => {
it('should ignore invalid @returns', () => {
const { extractedTags } = parseJsDoc('@returns');
expect(extractedTags.returns).toBeNull();
});
it('should return a @returns with a type', () => {
const { extractedTags } = parseJsDoc('@returns {string}');
expect(extractedTags.returns).not.toBeNull();
expect(extractedTags.returns.type).not.toBeNull();
expect(extractedTags.returns.type.name).toBe('string');
});
it('should return a @returns with a type and a description', () => {
const { extractedTags } = parseJsDoc('@returns {string} - A bar description');
expect(extractedTags.returns).not.toBeNull();
expect(extractedTags.returns.type).not.toBeNull();
expect(extractedTags.returns.type.name).toBe('string');
expect(extractedTags.returns.description).toBe('A bar description');
});
it('should support multiline @returns description', () => {
const { extractedTags } = parseJsDoc(
'@returns {string} - This is\na multiline\ndescription\n'
);
expect(extractedTags.returns).not.toBeNull();
expect(extractedTags.returns.type).not.toBeNull();
expect(extractedTags.returns.type.name).toBe('string');
expect(extractedTags.returns.description).toBe('This is\na multiline\ndescription');
});
it('should only consider the last @returns tag when there is multiple', () => {
const { extractedTags } = parseJsDoc('@returns {string}\n@returns {number}');
expect(extractedTags.returns).not.toBeNull();
expect(extractedTags.returns.type).not.toBeNull();
expect(extractedTags.returns.type.name).toBe('number');
});
describe('getTypeName', () => {
it('should support named type', () => {
const { extractedTags } = parseJsDoc('@returns {string}');
expect(extractedTags.returns.getTypeName()).toBe('string');
});
it('should support record type with a single field', () => {
const { extractedTags } = parseJsDoc('@returns {{a: number}}');
expect(extractedTags.returns.getTypeName()).toBe('({a: number})');
});
it('should support record type with multiple fields', () => {
const { extractedTags } = parseJsDoc('@returns {{a: number, b: string}}');
expect(extractedTags.returns.getTypeName()).toBe('({a: number, b: string})');
});
it('should support record type with a field having only a name', () => {
const { extractedTags } = parseJsDoc('@returns {{a}}');
expect(extractedTags.returns.getTypeName()).toBe('({a})');
});
it('should support array type', () => {
const { extractedTags } = parseJsDoc('@returns {integer[]}');
expect(extractedTags.returns.getTypeName()).toBe('integer[]');
});
it('should support untyped array type', () => {
const { extractedTags } = parseJsDoc('@returns {[]}');
expect(extractedTags.returns.getTypeName()).toBe('[]');
});
it('should support union type', () => {
const { extractedTags } = parseJsDoc('@returns {(number|boolean)}');
expect(extractedTags.returns.getTypeName()).toBe('(number|boolean)');
});
it('should support any type', () => {
const { extractedTags } = parseJsDoc('@returns {*}');
expect(extractedTags.returns.getTypeName()).toBe('any');
});
it('should support void', () => {
const { extractedTags } = parseJsDoc('@returns {void}');
expect(extractedTags.returns.getTypeName()).toBe('void');
});
});
});
it('should ignore unsupported JSDoc tags', () => {
const { extractedTags } = parseJsDoc('Hey!\n@param event', { tags: [] });
expect(extractedTags.params).toBeNull();
});
it('should remove extra newline characters between tags', () => {
const { extractedTags } = parseJsDoc(
'Hey!\n@param {SyntheticEvent} event - Original event.\n \n \n \n@returns {string}'
);
expect(extractedTags.params).not.toBeNull();
expect(Object.keys(extractedTags.params).length).toBe(1);
expect(extractedTags.params[0].name).toBe('event');
expect(extractedTags.params[0].type.name).toBe('SyntheticEvent');
expect(extractedTags.params[0].description).toBe('Original event.');
expect(extractedTags.returns).not.toBeNull();
expect(extractedTags.returns.type.name).toBe('string');
});
});

View File

@ -0,0 +1,234 @@
import doctrine, { Annotation } from 'doctrine';
import { isNil } from 'lodash';
export interface ExtractedJsDocParam {
name: string;
type?: any;
description?: string;
getPrettyName: () => string;
getTypeName: () => string;
}
export interface ExtractedJsDocReturns {
type?: any;
description?: string;
getTypeName: () => string;
}
export interface ExtractedJsDoc {
params?: ExtractedJsDocParam[];
returns?: ExtractedJsDocReturns;
ignore: boolean;
}
export interface JsDocParsingOptions {
tags?: string[];
}
export interface JsDocParsingResult {
includesJsDoc: boolean;
ignore: boolean;
description?: string;
extractedTags?: ExtractedJsDoc;
}
export type ParseJsDoc = (value?: string, options?: JsDocParsingOptions) => JsDocParsingResult;
function containsJsDoc(value?: string): boolean {
return !isNil(value) && value.includes('@');
}
function parse(content: string, tags: string[]): Annotation {
let ast;
try {
ast = doctrine.parse(content, {
tags,
sloppy: true,
});
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
throw new Error('Cannot parse JSDoc tags.');
}
return ast;
}
const DEFAULT_OPTIONS = {
tags: ['param', 'arg', 'argument', 'returns', 'ignore'],
};
export const parseJsDoc: ParseJsDoc = (
value?: string,
options: JsDocParsingOptions = DEFAULT_OPTIONS
) => {
if (!containsJsDoc(value)) {
return {
includesJsDoc: false,
ignore: false,
};
}
const jsDocAst = parse(value, options.tags);
const extractedTags = extractJsDocTags(jsDocAst);
if (extractedTags.ignore) {
// There is no point in doing other stuff since this prop will not be rendered.
return {
includesJsDoc: true,
ignore: true,
};
}
return {
includesJsDoc: true,
ignore: false,
// Always use the parsed description to ensure JSDoc is removed from the description.
description: jsDocAst.description,
extractedTags,
};
};
function extractJsDocTags(ast: doctrine.Annotation): ExtractedJsDoc {
const extractedTags: ExtractedJsDoc = {
params: null,
returns: null,
ignore: false,
};
for (let i = 0; i < ast.tags.length; i += 1) {
const tag = ast.tags[i];
if (tag.title === 'ignore') {
extractedTags.ignore = true;
// Once we reach an @ignore tag, there is no point in parsing the other tags since we will not render the prop.
break;
} else {
switch (tag.title) {
// arg & argument are aliases for param.
case 'param':
case 'arg':
case 'argument': {
const paramTag = extractParam(tag);
if (!isNil(paramTag)) {
if (isNil(extractedTags.params)) {
extractedTags.params = [];
}
extractedTags.params.push(paramTag);
}
break;
}
case 'returns': {
const returnsTag = extractReturns(tag);
if (!isNil(returnsTag)) {
extractedTags.returns = returnsTag;
}
break;
}
default:
break;
}
}
}
return extractedTags;
}
function extractParam(tag: doctrine.Tag): ExtractedJsDocParam {
const paramName = tag.name;
// When the @param doesn't have a name but have a type and a description, "null-null" is returned.
if (!isNil(paramName) && paramName !== 'null-null') {
return {
name: tag.name,
type: tag.type,
description: tag.description,
getPrettyName: () => {
if (paramName.includes('null')) {
// There is a few cases in which the returned param name contains "null".
// - @param {SyntheticEvent} event- Original SyntheticEvent
// - @param {SyntheticEvent} event.\n@returns {string}
return paramName.replace('-null', '').replace('.null', '');
}
return tag.name;
},
getTypeName: () => {
return !isNil(tag.type) ? extractTypeName(tag.type) : null;
},
};
}
return null;
}
function extractReturns(tag: doctrine.Tag): ExtractedJsDocReturns {
if (!isNil(tag.type)) {
return {
type: tag.type,
description: tag.description,
getTypeName: () => {
return extractTypeName(tag.type);
},
};
}
return null;
}
function extractTypeName(type: doctrine.Type): string {
if (type.type === 'NameExpression') {
return type.name;
}
if (type.type === 'RecordType') {
const recordFields = type.fields.map((field: doctrine.type.FieldType) => {
if (!isNil(field.value)) {
const valueTypeName = extractTypeName(field.value);
return `${field.key}: ${valueTypeName}`;
}
return field.key;
});
return `({${recordFields.join(', ')}})`;
}
if (type.type === 'UnionType') {
const unionElements = type.elements.map(extractTypeName);
return `(${unionElements.join('|')})`;
}
// Only support untyped array: []. Might add more support later if required.
if (type.type === 'ArrayType') {
return '[]';
}
if (type.type === 'TypeApplication') {
if (!isNil(type.expression)) {
if ((type.expression as doctrine.type.NameExpression).name === 'Array') {
const arrayType = extractTypeName(type.applications[0]);
return `${arrayType}[]`;
}
}
}
if (
type.type === 'NullableType' ||
type.type === 'NonNullableType' ||
type.type === 'OptionalType'
) {
return extractTypeName(type.expression);
}
if (type.type === 'AllLiteral') {
return 'any';
}
return null;
}

View File

@ -1,903 +0,0 @@
import { propTypesHandler, tsHandler, flowHandler, unknownHandler } from './type-system-handlers';
import { DocgenInfo } from './DocgenInfo';
const DEFAULT_PROP_NAME = 'propName';
const PROP_TYPES_STRING_TYPE = {
name: 'string',
};
const PROP_TYPES_FUNC_TYPE = {
name: 'func',
};
const TS_FUNC_TYPE = {
name: '() => void',
};
const TS_STRING_TYPE = {
name: 'string',
};
function createDocgenInfo(overrides: Record<string, any> = {}): DocgenInfo {
return {
type: null,
required: true,
description: 'A string prop',
defaultValue: {
value: 'default string',
},
...overrides,
};
}
describe('prop-types handler', () => {
it('should map defaults docgen info properly', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_STRING_TYPE,
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.name).toBe(DEFAULT_PROP_NAME);
expect(propDef.type.name).toBe(docgenInfo.type.name);
expect(propDef.description).toBe(docgenInfo.description);
expect(propDef.required).toBe(docgenInfo.required);
expect(propDef.defaultValue).toBe(docgenInfo.defaultValue.value);
});
describe('for all prop types', () => {
it('should handle prop without a description', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_STRING_TYPE,
description: undefined,
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.description).toBeUndefined();
});
it('should clean the description', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_STRING_TYPE,
description: 'onClick description\n@param {SyntheticEvent} event',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.description).toBe('onClick description');
});
it('should have an empty description when the description only contains JSDoc', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_STRING_TYPE,
description: '@param event',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.description).toBe('');
});
it('should not remove newline characters of multilines description without JSDoc tags', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_STRING_TYPE,
description: 'onClick description\nis a\nmulti-lines\ndescription',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.description).toBe('onClick description\nis a\nmulti-lines\ndescription');
});
it('should not remove newline characters of multilines description with JSDoc tags', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_STRING_TYPE,
description: 'onClick description\nis a\nmulti-lines\ndescription\n@param event',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.description).toBe('onClick description\nis a\nmulti-lines\ndescription');
});
it('should not remove markdown from description without JSDoc tags', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_STRING_TYPE,
description: 'onClick *emphasis*, **strong**, `formatted` description.',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.description).toBe('onClick *emphasis*, **strong**, `formatted` description.');
});
it('should not remove markdown from description with JSDoc tags', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_STRING_TYPE,
description: 'onClick *emphasis*, **strong**, `formatted` description.\n@param event',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.description).toBe('onClick *emphasis*, **strong**, `formatted` description.');
});
it('should not remove @ characters that does not match JSDoc tags', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_STRING_TYPE,
description: 'onClick @description@',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.description).toBe('onClick @description@');
});
it('should not set ignore to true when the property is not marked with @ignore', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_STRING_TYPE,
description: 'onClick description',
});
const { ignore } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(ignore).toBeFalsy();
});
it('should set ignore to true when the property is marked with @ignore', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_STRING_TYPE,
description: 'onClick description\n@ignore',
});
const { ignore } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(ignore).toBeTruthy();
});
});
describe('when the prop is a function', () => {
it("should have func as type when the props doesn't have a description", () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: undefined,
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.description).toBeUndefined();
expect(propDef.type.name).toBe('func');
});
it('should have func as type when the prop have a description without JSDoc', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: 'onClick description',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('func');
expect(propDef.description).toBe('onClick description');
});
it('should have an empty description when the description only contains JSDoc', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: '@param event',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.description).toBe('');
});
describe('when the description contains a @param tag', () => {
it('should have func as type when it is an invalid @param tag', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: 'onClick description\n@param',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('func');
expect(propDef.description).toBe('onClick description');
});
it('should have a func signature with a single arg as type when it is a @param tag with a name', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: 'onClick description\n@param event',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('(event)');
expect(propDef.description).toBe('onClick description');
});
it('should have a func signature with a single arg as type when it is a @param tag with a name and a type', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: 'onClick description\n@param {SyntheticEvent} event',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('(event: SyntheticEvent)');
expect(propDef.description).toBe('onClick description');
});
it('should have a func signature with a single arg as type when it is a @param tag with a name, a type and a desc', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description:
'onClick description\n@param {SyntheticEvent} event - Original SyntheticEvent',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('(event: SyntheticEvent)');
expect(propDef.description).toBe('onClick description');
});
it('should have func as type when it is @param tag without a name 1', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: 'onClick description\n@param - Original SyntheticEvent',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('func');
expect(propDef.description).toBe('onClick description');
});
it('should have func as type when it is @param tag without a name 2', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: 'onClick description\n@param {SyntheticEvent} - Original SyntheticEvent',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('func');
expect(propDef.description).toBe('onClick description');
});
it('should support param of record type with a single field', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: 'onClick description\n@param {{a: number}} event',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('(event: ({a: number}))');
expect(propDef.description).toBe('onClick description');
});
it('should support param of record type with multiple fields', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: 'onClick description\n@param {{a: number, b: string}} event',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('(event: ({a: number, b: string}))');
expect(propDef.description).toBe('onClick description');
});
it('should support param of record type with a field having only a name', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: 'onClick description\n@param {{a}} event',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('(event: ({a}))');
expect(propDef.description).toBe('onClick description');
});
it('should support param of union type', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: 'onClick description\n@param {(number|boolean)} event',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('(event: (number|boolean))');
expect(propDef.description).toBe('onClick description');
});
it('should support param of array type', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: 'onClick description\n@param {number[]} event',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('(event: number[])');
expect(propDef.description).toBe('onClick description');
});
it('should support param of untyped array type', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: 'onClick description\n@param {[]} event',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('(event: [])');
expect(propDef.description).toBe('onClick description');
});
it('should support param with a nullable type', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: 'onClick description\n@param {?number} event',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('(event: number)');
expect(propDef.description).toBe('onClick description');
});
it('should support param with a non nullable type', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: 'onClick description\n@param {!number} event',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('(event: number)');
expect(propDef.description).toBe('onClick description');
});
it('should support optional param 1', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: 'onClick description\n@param {number} [event]',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('(event: number)');
expect(propDef.description).toBe('onClick description');
});
it('should support optional param 2', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: 'onClick description\n@param {number=} event',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('(event: number)');
expect(propDef.description).toBe('onClick description');
});
it('should support param of type any', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: 'onClick description\n@param {*} event',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('(event: any)');
expect(propDef.description).toBe('onClick description');
});
it('should support multilines description when there is a @param', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: 'onClick description\nis a\nmulti-lines\ndescription\n@param event',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('(event)');
expect(propDef.description).toBe('onClick description\nis a\nmulti-lines\ndescription');
});
it('should support multilines @param description', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description:
'onClick description\n@param event - This is my param\nmultiline description\n@param customData',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('(event, customData)');
expect(propDef.description).toBe('onClick description');
});
it('should autofix missing space between the param name and the description separator', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description:
'onClick description\n@param {SyntheticEvent} event- Original SyntheticEvent',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('(event: SyntheticEvent)');
expect(propDef.description).toBe('onClick description');
});
it('should autofix param name ending with . followed by a @returns tag', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: 'onClick description\n@param {SyntheticEvent} event.\n',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('(event: SyntheticEvent)');
expect(propDef.description).toBe('onClick description');
});
it('should provide raw @param tags', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description:
'onClick description\n@param {SyntheticEvent} event - Original event.\n@param {string} value',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.description).toBe('onClick description');
expect(propDef.jsDocTags).toBeDefined();
expect(propDef.jsDocTags.params).toBeDefined();
expect(propDef.jsDocTags.params[0].name).toBe('event');
expect(propDef.jsDocTags.params[0].description).toBe('Original event.');
expect(propDef.jsDocTags.params[1].name).toBe('value');
expect(propDef.jsDocTags.params[1].description).toBeNull();
});
});
describe('when the description contains multiple @param tags', () => {
it('should have a func signature with multiple args as type', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description:
'onClick description\n@param {SyntheticEvent} event\n@param {string} customData',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('(event: SyntheticEvent, customData: string)');
expect(propDef.description).toBe('onClick description');
});
it('should ignore invalid @param tags', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description:
'onClick description\n@param {SyntheticEvent} event\n@param {string} customData\n@param {SyntheticEvent} - Original event',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('(event: SyntheticEvent, customData: string)');
expect(propDef.description).toBe('onClick description');
});
});
it('should support @arg alias', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: 'onClick description\n@arg event',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('(event)');
expect(propDef.description).toBe('onClick description');
});
it('should support @argument alias', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: 'onClick description\n@argument event',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('(event)');
expect(propDef.description).toBe('onClick description');
});
describe('when the description contains a @returns tag', () => {
it('should have func as type when it is an invalid @returns tag', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: 'onClick description\n@returns',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('func');
expect(propDef.description).toBe('onClick description');
});
it('should have a func signature with a return type as type when it is a @returns tag with a type', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: 'onClick description\n@returns {string}',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('() => string');
expect(propDef.description).toBe('onClick description');
});
it('should have a func signature with a return type as type when it is a @returns tag with a type and a description', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: 'onClick description\n@returns {string} - A custom return type',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('() => string');
expect(propDef.description).toBe('onClick description');
});
it('should have func as type when it is a @returns tag without a type and there is no params.', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: 'onClick description\n@returns - A custom return type',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('func');
expect(propDef.description).toBe('onClick description');
});
it('should have no return type when it is a @returns tag without a type and there is params.', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: 'onClick description\n@param event\n@returns - A custom return type',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('(event)');
expect(propDef.description).toBe('onClick description');
});
it('should have a full signature as type when there is a @param and a @returns tag 1', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description:
'onClick description\n@param {SyntheticEvent} event - Original event.\n@returns {string}',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('(event: SyntheticEvent) => string');
expect(propDef.description).toBe('onClick description');
});
it('should have a full signature as type when there is a @param and a @returns tag 2', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description:
'onClick description\n@param {SyntheticEvent} event - Original event.\n@param {string} customData\n@returns {string}',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('(event: SyntheticEvent, customData: string) => string');
expect(propDef.description).toBe('onClick description');
});
it('should only consider the last @returns tag when there is more than one', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: 'onClick description\n@returns {string}\n@returns {integer}',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('() => integer');
expect(propDef.description).toBe('onClick description');
});
it('should support returns of record type', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: 'onClick description\n@returns {{a: number, b: string}}',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('() => ({a: number, b: string})');
expect(propDef.description).toBe('onClick description');
});
it('should support returns of array type', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: 'onClick description\n@returns {integer[]}',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('() => integer[]');
expect(propDef.description).toBe('onClick description');
});
it('should support returns of union type', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: 'onClick description\n@returns {(number|boolean)}',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('() => (number|boolean)');
expect(propDef.description).toBe('onClick description');
});
it('should support returns of type any', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: 'onClick description\n@returns {*}',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('() => any');
expect(propDef.description).toBe('onClick description');
});
it('should support returns of type void', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description: 'onClick description\n@returns {void}',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('() => void');
expect(propDef.description).toBe('onClick description');
});
it('should provide raw @returns tags when a description is defined', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description:
'onClick description\n@param {SyntheticEvent} event - Original event.\n@returns {string} - An awesome string.',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.description).toBe('onClick description');
expect(propDef.jsDocTags).toBeDefined();
expect(propDef.jsDocTags.returns).toBeDefined();
expect(propDef.jsDocTags.returns.description).toBe('An awesome string.');
});
it('should provide raw @returns tags when there is no description', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description:
'onClick description\n@param {SyntheticEvent} event - Original event.\n@returns {string}',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.description).toBe('onClick description');
expect(propDef.jsDocTags).toBeDefined();
expect(propDef.jsDocTags.returns).toBeDefined();
expect(propDef.jsDocTags.returns.description).toBeNull();
});
});
it('should remove extra newline characters between tags', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description:
'onClick description\n@param {SyntheticEvent} event - Original event.\n \n \n \n@returns {string}',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('(event: SyntheticEvent) => string');
expect(propDef.description).toBe('onClick description');
});
it('should ignore unsupported JSDoc tags', () => {
const docgenInfo = createDocgenInfo({
type: PROP_TYPES_FUNC_TYPE,
description:
'onClick description\n@param {SyntheticEvent} event\n@type {number}\n@returns {string}\n@version 2',
});
const { propDef } = propTypesHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.type.name).toBe('(event: SyntheticEvent) => string');
expect(propDef.description).toBe('onClick description');
});
});
});
describe('ts handler', () => {
it('should map defaults docgen info properly', () => {
const docgenInfo = createDocgenInfo({
tsType: PROP_TYPES_STRING_TYPE,
});
const { propDef } = tsHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.name).toBe(DEFAULT_PROP_NAME);
expect(propDef.type.name).toBe(docgenInfo.tsType.name);
expect(propDef.description).toBe(docgenInfo.description);
expect(propDef.required).toBe(docgenInfo.required);
expect(propDef.defaultValue).toBe(docgenInfo.defaultValue.value);
});
it('should provide raw @param tags', () => {
const docgenInfo = createDocgenInfo({
tsType: TS_FUNC_TYPE,
description:
'onClick description\n@param {SyntheticEvent} event - Original event.\n@param {string} value',
});
const { propDef } = tsHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.description).toBe('onClick description');
expect(propDef.jsDocTags).toBeDefined();
expect(propDef.jsDocTags.params).toBeDefined();
expect(propDef.jsDocTags.params[0].name).toBe('event');
expect(propDef.jsDocTags.params[0].description).toBe('Original event.');
expect(propDef.jsDocTags.params[1].name).toBe('value');
expect(propDef.jsDocTags.params[1].description).toBeNull();
});
it('should provide raw @returns tags when a description is defined', () => {
const docgenInfo = createDocgenInfo({
tsType: TS_FUNC_TYPE,
description:
'onClick description\n@param {SyntheticEvent} event - Original event.\n@returns {string} - An awesome string.',
});
const { propDef } = tsHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.description).toBe('onClick description');
expect(propDef.jsDocTags).toBeDefined();
expect(propDef.jsDocTags.returns).toBeDefined();
expect(propDef.jsDocTags.returns.description).toBe('An awesome string.');
});
it('should provide raw @returns tags when there is no description', () => {
const docgenInfo = createDocgenInfo({
tsType: TS_FUNC_TYPE,
description:
'onClick description\n@param {SyntheticEvent} event - Original event.\n@returns {string}',
});
const { propDef } = tsHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.description).toBe('onClick description');
expect(propDef.jsDocTags).toBeDefined();
expect(propDef.jsDocTags.returns).toBeDefined();
expect(propDef.jsDocTags.returns.description).toBeNull();
});
it('should not set ignore to true when the property is not marked with @ignore', () => {
const docgenInfo = createDocgenInfo({
tsType: TS_STRING_TYPE,
description: 'onClick description',
});
const { ignore } = tsHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(ignore).toBeFalsy();
});
it('should set ignore to true when the property is marked with @ignore', () => {
const docgenInfo = createDocgenInfo({
tsType: TS_STRING_TYPE,
description: 'onClick description\n@ignore',
});
const { ignore } = tsHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(ignore).toBeTruthy();
});
});
describe('flow handler', () => {
it('should map defaults docgen info properly', () => {
const docgenInfo = createDocgenInfo({
flowType: {
name: 'string',
},
});
const { propDef } = flowHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.name).toBe(DEFAULT_PROP_NAME);
expect(propDef.type.name).toBe(docgenInfo.flowType.name);
expect(propDef.description).toBe(docgenInfo.description);
expect(propDef.required).toBe(docgenInfo.required);
expect(propDef.defaultValue).toBe(docgenInfo.defaultValue.value);
});
it('should not set ignore to true when the property is not marked with @ignore', () => {
const docgenInfo = createDocgenInfo({
flowType: {
name: 'string',
},
description: 'onClick description',
});
const { ignore } = flowHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(ignore).toBeFalsy();
});
it('should set ignore to true when the property is marked with @ignore', () => {
const docgenInfo = createDocgenInfo({
flowType: {
name: 'string',
},
description: 'onClick description\n@ignore',
});
const { ignore } = flowHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(ignore).toBeTruthy();
});
});
describe('unknown handler', () => {
it('should map defaults docgen info properly', () => {
const docgenInfo = createDocgenInfo();
const { propDef } = unknownHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(propDef.name).toBe(DEFAULT_PROP_NAME);
expect(propDef.type.name).toBe('unknown');
expect(propDef.description).toBe(docgenInfo.description);
expect(propDef.required).toBe(docgenInfo.required);
expect(propDef.defaultValue).toBe(docgenInfo.defaultValue.value);
});
it('should not set ignore to true when the property is not marked with @ignore', () => {
const docgenInfo = createDocgenInfo({
description: 'onClick description',
});
const { ignore } = unknownHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(ignore).toBeFalsy();
});
it('should set ignore to true when the property is marked with @ignore', () => {
const docgenInfo = createDocgenInfo({
description: 'onClick description\n@ignore',
});
const { ignore } = unknownHandler(DEFAULT_PROP_NAME, docgenInfo);
expect(ignore).toBeTruthy();
});
});

View File

@ -1,178 +0,0 @@
import { PropDef } from '@storybook/components';
import { isNil } from 'lodash';
import { parseJsDoc, ExtractedJsDocTags, ExtractedJsDocParamTag } from './jsdoc-parser';
import { DocgenInfo } from './DocgenInfo';
export const TypeSystem = {
Flow: 'Flow',
TypeScript: 'TypeScript',
PropTypes: 'PropTypes',
Unknown: 'Unknown',
};
export interface TypeSystemHandlerResult {
propDef?: PropDef;
ignore: boolean;
}
export type TypeSystemHandler = (
propName: string,
docgenInfo: DocgenInfo
) => TypeSystemHandlerResult;
interface HandlePropResult extends TypeSystemHandlerResult {
extractedJsDocTags?: ExtractedJsDocTags;
}
function createDefaultPropDef(
propName: string,
propType: {
name: string;
},
docgenInfo: DocgenInfo
): PropDef {
const { description, required, defaultValue } = docgenInfo;
return {
name: propName,
type: propType,
required,
description,
defaultValue: isNil(defaultValue) ? null : defaultValue.value,
};
}
function propMightContainsJsDoc(docgenInfo: DocgenInfo): boolean {
return !isNil(docgenInfo.description) && docgenInfo.description.includes('@');
}
function handleProp(
propName: string,
propType: {
name: string;
},
docgenInfo: DocgenInfo
): HandlePropResult {
const propDef = createDefaultPropDef(propName, propType, docgenInfo);
if (propMightContainsJsDoc(docgenInfo)) {
const { ignore, description, extractedTags } = parseJsDoc(docgenInfo);
if (ignore) {
return {
ignore: true,
};
}
if (!isNil(description)) {
propDef.description = description;
}
const hasParams = !isNil(extractedTags.params);
const hasReturns = !isNil(extractedTags.returns) && !isNil(extractedTags.returns.type);
if (hasParams || hasReturns) {
propDef.jsDocTags = {
params: hasParams && extractedTags.params.map(x => x.raw),
returns: hasReturns && extractedTags.returns.raw,
};
return {
propDef,
extractedJsDocTags: extractedTags,
ignore: false,
};
}
}
return {
propDef,
ignore: false,
};
}
export const propTypesHandler: TypeSystemHandler = (propName: string, docgenInfo: DocgenInfo) => {
const result = handleProp(propName, docgenInfo.type, docgenInfo);
if (!result.ignore) {
const { propDef, extractedJsDocTags } = result;
// When available use the proper JSDoc tags to describe the function signature instead of displaying "func".
if (propDef.type.name === 'func') {
if (!isNil(extractedJsDocTags)) {
const hasParams = !isNil(extractedJsDocTags.params);
const hasReturns = !isNil(extractedJsDocTags.returns);
if (hasParams || hasReturns) {
const funcParts = [];
if (hasParams) {
const funcParams = extractedJsDocTags.params.map((x: ExtractedJsDocParamTag) => {
const prettyName = x.getPrettyName();
const typeName = x.getTypeName();
if (!isNil(typeName)) {
return `${prettyName}: ${typeName}`;
}
return prettyName;
});
funcParts.push(`(${funcParams.join(', ')})`);
} else {
funcParts.push('()');
}
if (hasReturns) {
funcParts.push(`=> ${extractedJsDocTags.returns.getTypeName()}`);
}
propDef.type = {
name: funcParts.join(' '),
};
}
}
}
}
return result;
};
export const tsHandler: TypeSystemHandler = (propName: string, docgenInfo: DocgenInfo) => {
return handleProp(propName, docgenInfo.tsType, docgenInfo);
};
export const flowHandler: TypeSystemHandler = (propName: string, docgenInfo: DocgenInfo) => {
return handleProp(propName, docgenInfo.flowType, docgenInfo);
};
export const unknownHandler: TypeSystemHandler = (propName: string, docgenInfo: DocgenInfo) => {
return handleProp(propName, { name: 'unknown' }, docgenInfo);
};
export const TypeSystemHandlers: Record<string, TypeSystemHandler> = {
[TypeSystem.Flow]: flowHandler,
[TypeSystem.TypeScript]: tsHandler,
[TypeSystem.PropTypes]: propTypesHandler,
[TypeSystem.Unknown]: unknownHandler,
};
export const getPropTypeSystem = (docgenInfo: DocgenInfo): string => {
if (!isNil(docgenInfo.flowType)) {
return TypeSystem.Flow;
}
if (!isNil(docgenInfo.tsType)) {
return TypeSystem.TypeScript;
}
if (!isNil(docgenInfo.type)) {
return TypeSystem.PropTypes;
}
return TypeSystem.Unknown;
};
export const getTypeSystemHandler = (typeSystem: string): TypeSystemHandler => {
return TypeSystemHandlers[typeSystem];
};

View File

@ -1,849 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs-mdx-compiler-plugin decorators.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Button } from '@storybook/react/demo';
import { Story, Meta } from '@storybook/addon-docs/blocks';
const makeShortcode = name =>
function MDXDefaultShortcode(props) {
console.warn(
'Component ' +
name +
' was not imported, exported, or provided by MDXProvider as global scope'
);
return <div {...props} />;
};
const layoutProps = {};
const MDXLayout = 'wrapper';
function MDXContent({ components, ...props }) {
return (
<MDXLayout {...layoutProps} {...props} components={components} mdxType=\\"MDXLayout\\">
<Meta
title=\\"Button\\"
decorators={[
storyFn => (
<div
style={{
backgroundColor: 'yellow',
}}
>
{storyFn()}
</div>
),
]}
mdxType=\\"Meta\\"
/>
<h1>{\`Decorated story\`}</h1>
<Story
name=\\"one\\"
decorators={[storyFn => <div className=\\"local\\">{storyFn()}</div>]}
mdxType=\\"Story\\"
>
<Button mdxType=\\"Button\\">One</Button>
</Story>
</MDXLayout>
);
}
MDXContent.isMDXComponent = true;
export const one = () => <Button>One</Button>;
one.story = {};
one.story.name = 'one';
one.story.parameters = { mdxSource: '<Button>One</Button>' };
one.story.decorators = [storyFn => <div className=\\"local\\">{storyFn()}</div>];
const componentMeta = {
title: 'Button',
decorators: [
storyFn => (
<div
style={{
backgroundColor: 'yellow',
}}
>
{storyFn()}
</div>
),
],
includeStories: ['one'],
};
const mdxStoryNameToId = { one: 'button--one' };
componentMeta.parameters = componentMeta.parameters || {};
componentMeta.parameters.docs = {
container: ({ context, children }) => (
<DocsContainer context={{ ...context, mdxStoryNameToId }}>{children}</DocsContainer>
),
page: MDXContent,
};
export default componentMeta;
"
`;
exports[`docs-mdx-compiler-plugin docs-only.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Meta } from '@storybook/addon-docs/blocks';
const makeShortcode = name =>
function MDXDefaultShortcode(props) {
console.warn(
'Component ' +
name +
' was not imported, exported, or provided by MDXProvider as global scope'
);
return <div {...props} />;
};
const layoutProps = {};
const MDXLayout = 'wrapper';
function MDXContent({ components, ...props }) {
return (
<MDXLayout {...layoutProps} {...props} components={components} mdxType=\\"MDXLayout\\">
<Meta title=\\"docs-only\\" mdxType=\\"Meta\\" />
<h1>{\`Documentation only\`}</h1>
<p>
{\`This is a documentation-only MDX file which generates a dummy \`}
<inlineCode parentName=\\"p\\">{\`docsOnly: true\`}</inlineCode>
{\` story.\`}
</p>
</MDXLayout>
);
}
MDXContent.isMDXComponent = true;
export const __page = () => {
throw new Error('Docs-only story');
};
__page.story = { parameters: { docsOnly: true } };
const componentMeta = { title: 'docs-only', includeStories: ['__page'] };
const mdxStoryNameToId = {};
componentMeta.parameters = componentMeta.parameters || {};
componentMeta.parameters.docs = {
container: ({ context, children }) => (
<DocsContainer context={{ ...context, mdxStoryNameToId }}>{children}</DocsContainer>
),
page: MDXContent,
};
export default componentMeta;
"
`;
exports[`docs-mdx-compiler-plugin non-story-exports.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Button } from '@storybook/react/demo';
import { Story, Meta } from '@storybook/addon-docs/blocks';
export const two = 2;
const makeShortcode = name =>
function MDXDefaultShortcode(props) {
console.warn(
'Component ' +
name +
' was not imported, exported, or provided by MDXProvider as global scope'
);
return <div {...props} />;
};
const layoutProps = {
two,
};
const MDXLayout = 'wrapper';
function MDXContent({ components, ...props }) {
return (
<MDXLayout {...layoutProps} {...props} components={components} mdxType=\\"MDXLayout\\">
<Meta title=\\"Button\\" mdxType=\\"Meta\\" />
<h1>{\`Story definition\`}</h1>
<Story name=\\"one\\" mdxType=\\"Story\\">
<Button mdxType=\\"Button\\">One</Button>
</Story>
<Story name=\\"hello story\\" mdxType=\\"Story\\">
<Button mdxType=\\"Button\\">Hello button</Button>
</Story>
</MDXLayout>
);
}
MDXContent.isMDXComponent = true;
export const one = () => <Button>One</Button>;
one.story = {};
one.story.name = 'one';
one.story.parameters = { mdxSource: '<Button>One</Button>' };
export const helloStory = () => <Button>Hello button</Button>;
helloStory.story = {};
helloStory.story.name = 'hello story';
helloStory.story.parameters = { mdxSource: '<Button>Hello button</Button>' };
const componentMeta = { title: 'Button', includeStories: ['one', 'helloStory'] };
const mdxStoryNameToId = { one: 'button--one', 'hello story': 'button--hello-story' };
componentMeta.parameters = componentMeta.parameters || {};
componentMeta.parameters.docs = {
container: ({ context, children }) => (
<DocsContainer context={{ ...context, mdxStoryNameToId }}>{children}</DocsContainer>
),
page: MDXContent,
};
export default componentMeta;
"
`;
exports[`docs-mdx-compiler-plugin parameters.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Button } from '@storybook/react/demo';
import { Story, Meta } from '@storybook/addon-docs/blocks';
const makeShortcode = name =>
function MDXDefaultShortcode(props) {
console.warn(
'Component ' +
name +
' was not imported, exported, or provided by MDXProvider as global scope'
);
return <div {...props} />;
};
const layoutProps = {};
const MDXLayout = 'wrapper';
function MDXContent({ components, ...props }) {
return (
<MDXLayout {...layoutProps} {...props} components={components} mdxType=\\"MDXLayout\\">
<Meta
title=\\"Button\\"
component={Button}
parameters={{
notes: 'component notes',
}}
mdxType=\\"Meta\\"
/>
<Story name=\\"component notes\\" mdxType=\\"Story\\">
<Button mdxType=\\"Button\\">Component notes</Button>
</Story>
<Story
name=\\"story notes\\"
parameters={{
notes: 'story notes',
}}
mdxType=\\"Story\\"
>
<Button mdxType=\\"Button\\">Story notes</Button>
</Story>
</MDXLayout>
);
}
MDXContent.isMDXComponent = true;
export const componentNotes = () => <Button>Component notes</Button>;
componentNotes.story = {};
componentNotes.story.name = 'component notes';
componentNotes.story.parameters = { mdxSource: '<Button>Component notes</Button>' };
export const storyNotes = () => <Button>Story notes</Button>;
storyNotes.story = {};
storyNotes.story.name = 'story notes';
storyNotes.story.parameters = {
mdxSource: '<Button>Story notes</Button>',
...{
notes: 'story notes',
},
};
const componentMeta = {
title: 'Button',
parameters: {
notes: 'component notes',
},
includeStories: ['componentNotes', 'storyNotes'],
};
const mdxStoryNameToId = {
'component notes': 'button--component-notes',
'story notes': 'button--story-notes',
};
componentMeta.parameters = componentMeta.parameters || {};
componentMeta.parameters.docs = {
container: ({ context, children }) => (
<DocsContainer context={{ ...context, mdxStoryNameToId }}>{children}</DocsContainer>
),
page: MDXContent,
};
export default componentMeta;
"
`;
exports[`docs-mdx-compiler-plugin previews.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Button } from '@storybook/react/demo';
import { Preview, Story, Meta } from '@storybook/addon-docs/blocks';
const makeShortcode = name =>
function MDXDefaultShortcode(props) {
console.warn(
'Component ' +
name +
' was not imported, exported, or provided by MDXProvider as global scope'
);
return <div {...props} />;
};
const layoutProps = {};
const MDXLayout = 'wrapper';
function MDXContent({ components, ...props }) {
return (
<MDXLayout {...layoutProps} {...props} components={components} mdxType=\\"MDXLayout\\">
<Meta
title=\\"Button\\"
component={Button}
parameters={{
notes: 'component notes',
}}
mdxType=\\"Meta\\"
/>
<h1>{\`Preview\`}</h1>
<p>{\`Previews can contain normal components, stories, and story references\`}</p>
<Preview mdxType=\\"Preview\\">
<Button mdxType=\\"Button\\">Just a button</Button>
<Story name=\\"hello button\\" mdxType=\\"Story\\">
<Button mdxType=\\"Button\\">Hello button</Button>
</Story>
<Story name=\\"two\\" mdxType=\\"Story\\">
<Button mdxType=\\"Button\\">Two</Button>
</Story>
<Story id=\\"welcome--welcome\\" mdxType=\\"Story\\" />
</Preview>
<p>{\`Preview wthout a story\`}</p>
<Preview mdxSource=\\"%0A%3CButton%3EJust%20a%20button%3C/Button%3E%0A\\" mdxType=\\"Preview\\">
<Button mdxType=\\"Button\\">Just a button</Button>
</Preview>
</MDXLayout>
);
}
MDXContent.isMDXComponent = true;
export const helloButton = () => <Button>Hello button</Button>;
helloButton.story = {};
helloButton.story.name = 'hello button';
helloButton.story.parameters = { mdxSource: '<Button>Hello button</Button>' };
export const two = () => <Button>Two</Button>;
two.story = {};
two.story.name = 'two';
two.story.parameters = { mdxSource: '<Button>Two</Button>' };
const componentMeta = {
title: 'Button',
parameters: {
notes: 'component notes',
},
includeStories: ['helloButton', 'two'],
};
const mdxStoryNameToId = { 'hello button': 'button--hello-button', two: 'button--two' };
componentMeta.parameters = componentMeta.parameters || {};
componentMeta.parameters.docs = {
container: ({ context, children }) => (
<DocsContainer context={{ ...context, mdxStoryNameToId }}>{children}</DocsContainer>
),
page: MDXContent,
};
export default componentMeta;
"
`;
exports[`docs-mdx-compiler-plugin story-current.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Story } from '@storybook/addon-docs/blocks';
const makeShortcode = name =>
function MDXDefaultShortcode(props) {
console.warn(
'Component ' +
name +
' was not imported, exported, or provided by MDXProvider as global scope'
);
return <div {...props} />;
};
const layoutProps = {};
const MDXLayout = 'wrapper';
function MDXContent({ components, ...props }) {
return (
<MDXLayout {...layoutProps} {...props} components={components} mdxType=\\"MDXLayout\\">
<h1>{\`Current story\`}</h1>
<Story id=\\".\\" mdxType=\\"Story\\" />
</MDXLayout>
);
}
MDXContent.isMDXComponent = true;
const componentMeta = { includeStories: [] };
const mdxStoryNameToId = {};
componentMeta.parameters = componentMeta.parameters || {};
componentMeta.parameters.docs = {
container: ({ context, children }) => (
<DocsContainer context={{ ...context, mdxStoryNameToId }}>{children}</DocsContainer>
),
page: MDXContent,
};
export default componentMeta;
"
`;
exports[`docs-mdx-compiler-plugin story-def-text-only.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Story, Meta } from '@storybook/addon-docs/blocks';
const makeShortcode = name =>
function MDXDefaultShortcode(props) {
console.warn(
'Component ' +
name +
' was not imported, exported, or provided by MDXProvider as global scope'
);
return <div {...props} />;
};
const layoutProps = {};
const MDXLayout = 'wrapper';
function MDXContent({ components, ...props }) {
return (
<MDXLayout {...layoutProps} {...props} components={components} mdxType=\\"MDXLayout\\">
<Meta title=\\"Text\\" mdxType=\\"Meta\\" />
<h1>{\`Story definition\`}</h1>
<Story name=\\"text\\" mdxType=\\"Story\\">
Plain text
</Story>
</MDXLayout>
);
}
MDXContent.isMDXComponent = true;
export const text = makeStoryFn('Plain text');
text.story = {};
text.story.name = 'text';
text.story.parameters = { mdxSource: \\"'Plain text'\\" };
const componentMeta = { title: 'Text', includeStories: ['text'] };
const mdxStoryNameToId = { text: 'text--text' };
componentMeta.parameters = componentMeta.parameters || {};
componentMeta.parameters.docs = {
container: ({ context, children }) => (
<DocsContainer context={{ ...context, mdxStoryNameToId }}>{children}</DocsContainer>
),
page: MDXContent,
};
export default componentMeta;
"
`;
exports[`docs-mdx-compiler-plugin story-definitions.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Button } from '@storybook/react/demo';
import { Story, Meta } from '@storybook/addon-docs/blocks';
const makeShortcode = name =>
function MDXDefaultShortcode(props) {
console.warn(
'Component ' +
name +
' was not imported, exported, or provided by MDXProvider as global scope'
);
return <div {...props} />;
};
const layoutProps = {};
const MDXLayout = 'wrapper';
function MDXContent({ components, ...props }) {
return (
<MDXLayout {...layoutProps} {...props} components={components} mdxType=\\"MDXLayout\\">
<Meta title=\\"Button\\" mdxType=\\"Meta\\" />
<h1>{\`Story definition\`}</h1>
<Story name=\\"one\\" mdxType=\\"Story\\">
<Button mdxType=\\"Button\\">One</Button>
</Story>
<Story name=\\"hello story\\" mdxType=\\"Story\\">
<Button mdxType=\\"Button\\">Hello button</Button>
</Story>
<Story name=\\"w/punctuation\\" mdxType=\\"Story\\">
<Button mdxType=\\"Button\\">with punctuation</Button>
</Story>
<Story name=\\"1 fine day\\" mdxType=\\"Story\\">
<Button mdxType=\\"Button\\">starts with number</Button>
</Story>
</MDXLayout>
);
}
MDXContent.isMDXComponent = true;
export const one = () => <Button>One</Button>;
one.story = {};
one.story.name = 'one';
one.story.parameters = { mdxSource: '<Button>One</Button>' };
export const helloStory = () => <Button>Hello button</Button>;
helloStory.story = {};
helloStory.story.name = 'hello story';
helloStory.story.parameters = { mdxSource: '<Button>Hello button</Button>' };
export const wPunctuation = () => <Button>with punctuation</Button>;
wPunctuation.story = {};
wPunctuation.story.name = 'w/punctuation';
wPunctuation.story.parameters = { mdxSource: '<Button>with punctuation</Button>' };
export const _1FineDay = () => <Button>starts with number</Button>;
_1FineDay.story = {};
_1FineDay.story.name = '1 fine day';
_1FineDay.story.parameters = { mdxSource: '<Button>starts with number</Button>' };
const componentMeta = {
title: 'Button',
includeStories: ['one', 'helloStory', 'wPunctuation', '_1FineDay'],
};
const mdxStoryNameToId = {
one: 'button--one',
'hello story': 'button--hello-story',
'w/punctuation': 'button--w-punctuation',
'1 fine day': 'button--1-fine-day',
};
componentMeta.parameters = componentMeta.parameters || {};
componentMeta.parameters.docs = {
container: ({ context, children }) => (
<DocsContainer context={{ ...context, mdxStoryNameToId }}>{children}</DocsContainer>
),
page: MDXContent,
};
export default componentMeta;
"
`;
exports[`docs-mdx-compiler-plugin story-function.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
const makeShortcode = name =>
function MDXDefaultShortcode(props) {
console.warn(
'Component ' +
name +
' was not imported, exported, or provided by MDXProvider as global scope'
);
return <div {...props} />;
};
const Story = makeShortcode('Story');
const layoutProps = {};
const MDXLayout = 'wrapper';
function MDXContent({ components, ...props }) {
return (
<MDXLayout {...layoutProps} {...props} components={components} mdxType=\\"MDXLayout\\">
<Story name=\\"function\\" height=\\"100px\\" mdxType=\\"Story\\">
{() => {
const btn = document.createElement('button');
btn.innerHTML = 'Hello Button';
btn.addEventListener('click', action('Click'));
return btn;
}}
</Story>
</MDXLayout>
);
}
MDXContent.isMDXComponent = true;
export const functionStory = makeStoryFn(() => {
const btn = document.createElement('button');
btn.innerHTML = 'Hello Button';
btn.addEventListener('click', action('Click'));
return btn;
});
functionStory.story = {};
functionStory.story.name = 'function';
functionStory.story.parameters = {
mdxSource:
\\"() => {\\\\n const btn = document.createElement('button');\\\\n btn.innerHTML = 'Hello Button';\\\\n btn.addEventListener('click', action('Click'));\\\\n return btn;\\\\n}\\",
};
const componentMeta = { includeStories: ['functionStory'] };
const mdxStoryNameToId = {};
componentMeta.parameters = componentMeta.parameters || {};
componentMeta.parameters.docs = {
container: ({ context, children }) => (
<DocsContainer context={{ ...context, mdxStoryNameToId }}>{children}</DocsContainer>
),
page: MDXContent,
};
export default componentMeta;
"
`;
exports[`docs-mdx-compiler-plugin story-function-var.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Meta, Story } from '@storybook/addon-docs/blocks';
export const basicFn = () => <Button mdxType=\\"Button\\" />;
const makeShortcode = name =>
function MDXDefaultShortcode(props) {
console.warn(
'Component ' +
name +
' was not imported, exported, or provided by MDXProvider as global scope'
);
return <div {...props} />;
};
const Button = makeShortcode('Button');
const layoutProps = {
basicFn,
};
const MDXLayout = 'wrapper';
function MDXContent({ components, ...props }) {
return (
<MDXLayout {...layoutProps} {...props} components={components} mdxType=\\"MDXLayout\\">
<Meta title=\\"story-function-var\\" mdxType=\\"Meta\\" />
<h1>{\`Button\`}</h1>
<p>{\`I can define a story with the function defined in CSF:\`}</p>
<Story name=\\"basic\\" mdxType=\\"Story\\">
{basicFn}
</Story>
</MDXLayout>
);
}
MDXContent.isMDXComponent = true;
export const basic = makeStoryFn(basicFn);
basic.story = {};
basic.story.name = 'basic';
basic.story.parameters = { mdxSource: 'basicFn' };
const componentMeta = { title: 'story-function-var', includeStories: ['basic'] };
const mdxStoryNameToId = { basic: 'story-function-var--basic' };
componentMeta.parameters = componentMeta.parameters || {};
componentMeta.parameters.docs = {
container: ({ context, children }) => (
<DocsContainer context={{ ...context, mdxStoryNameToId }}>{children}</DocsContainer>
),
page: MDXContent,
};
export default componentMeta;
"
`;
exports[`docs-mdx-compiler-plugin story-object.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Story, Meta } from '@storybook/addon-docs/blocks';
import { Welcome, Button } from '@storybook/angular/demo';
import { linkTo } from '@storybook/addon-links';
const makeShortcode = name =>
function MDXDefaultShortcode(props) {
console.warn(
'Component ' +
name +
' was not imported, exported, or provided by MDXProvider as global scope'
);
return <div {...props} />;
};
const layoutProps = {};
const MDXLayout = 'wrapper';
function MDXContent({ components, ...props }) {
return (
<MDXLayout {...layoutProps} {...props} components={components} mdxType=\\"MDXLayout\\">
<Meta title=\\"MDX|Welcome\\" mdxType=\\"Meta\\" />
<h1>{\`Story object\`}</h1>
<Story name=\\"to storybook\\" height=\\"300px\\" mdxType=\\"Story\\">
{{
template: \`<storybook-welcome-component (showApp)=\\"showApp()\\"></storybook-welcome-component>\`,
props: {
showApp: linkTo('Button'),
},
moduleMetadata: {
declarations: [Welcome],
},
}}
</Story>
</MDXLayout>
);
}
MDXContent.isMDXComponent = true;
export const toStorybook = makeStoryFn({
template: \`<storybook-welcome-component (showApp)=\\"showApp()\\"></storybook-welcome-component>\`,
props: {
showApp: linkTo('Button'),
},
moduleMetadata: {
declarations: [Welcome],
},
});
toStorybook.story = {};
toStorybook.story.name = 'to storybook';
toStorybook.story.parameters = {
mdxSource:
'{\\\\n template: \`<storybook-welcome-component (showApp)=\\"showApp()\\"></storybook-welcome-component>\`,\\\\n props: {\\\\n showApp: linkTo(\\\\'Button\\\\')\\\\n },\\\\n moduleMetadata: {\\\\n declarations: [Welcome]\\\\n }\\\\n}',
};
const componentMeta = { title: 'MDX|Welcome', includeStories: ['toStorybook'] };
const mdxStoryNameToId = { 'to storybook': 'mdx-welcome--to-storybook' };
componentMeta.parameters = componentMeta.parameters || {};
componentMeta.parameters.docs = {
container: ({ context, children }) => (
<DocsContainer context={{ ...context, mdxStoryNameToId }}>{children}</DocsContainer>
),
page: MDXContent,
};
export default componentMeta;
"
`;
exports[`docs-mdx-compiler-plugin story-references.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Story } from '@storybook/addon-docs/blocks';
const makeShortcode = name =>
function MDXDefaultShortcode(props) {
console.warn(
'Component ' +
name +
' was not imported, exported, or provided by MDXProvider as global scope'
);
return <div {...props} />;
};
const layoutProps = {};
const MDXLayout = 'wrapper';
function MDXContent({ components, ...props }) {
return (
<MDXLayout {...layoutProps} {...props} components={components} mdxType=\\"MDXLayout\\">
<h1>{\`Story reference\`}</h1>
<Story id=\\"welcome--welcome\\" mdxType=\\"Story\\" />
</MDXLayout>
);
}
MDXContent.isMDXComponent = true;
const componentMeta = { includeStories: [] };
const mdxStoryNameToId = {};
componentMeta.parameters = componentMeta.parameters || {};
componentMeta.parameters.docs = {
container: ({ context, children }) => (
<DocsContainer context={{ ...context, mdxStoryNameToId }}>{children}</DocsContainer>
),
page: MDXContent,
};
export default componentMeta;
"
`;
exports[`docs-mdx-compiler-plugin vanilla.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Button } from '@storybook/react/demo';
const makeShortcode = name =>
function MDXDefaultShortcode(props) {
console.warn(
'Component ' +
name +
' was not imported, exported, or provided by MDXProvider as global scope'
);
return <div {...props} />;
};
const layoutProps = {};
const MDXLayout = 'wrapper';
function MDXContent({ components, ...props }) {
return (
<MDXLayout {...layoutProps} {...props} components={components} mdxType=\\"MDXLayout\\">
<h1>{\`Hello MDX\`}</h1>
<p>{\`This is some random content.\`}</p>
<Button mdxType=\\"Button\\">Hello button</Button>
</MDXLayout>
);
}
MDXContent.isMDXComponent = true;
const componentMeta = { includeStories: [] };
const mdxStoryNameToId = {};
componentMeta.parameters = componentMeta.parameters || {};
componentMeta.parameters.docs = {
container: ({ context, children }) => (
<DocsContainer context={{ ...context, mdxStoryNameToId }}>{children}</DocsContainer>
),
page: MDXContent,
};
export default componentMeta;
"
`;

View File

@ -0,0 +1,8 @@
import { Button } from '@storybook/react/demo';
import { Story, Meta } from '@storybook/addon-docs/blocks';
<Meta title="Button" component={Button} id="button-id" />
<Story name="component notes">
<Button>Component notes</Button>
</Story>

View File

@ -0,0 +1,54 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs-mdx-compiler-plugin component-id.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Button } from '@storybook/react/demo';
import { Story, Meta } from '@storybook/addon-docs/blocks';
const makeShortcode = name =>
function MDXDefaultShortcode(props) {
console.warn(
'Component ' +
name +
' was not imported, exported, or provided by MDXProvider as global scope'
);
return <div {...props} />;
};
const layoutProps = {};
const MDXLayout = 'wrapper';
function MDXContent({ components, ...props }) {
return (
<MDXLayout {...layoutProps} {...props} components={components} mdxType=\\"MDXLayout\\">
<Meta title=\\"Button\\" component={Button} id=\\"button-id\\" mdxType=\\"Meta\\" />
<Story name=\\"component notes\\" mdxType=\\"Story\\">
<Button mdxType=\\"Button\\">Component notes</Button>
</Story>
</MDXLayout>
);
}
MDXContent.isMDXComponent = true;
export const componentNotes = () => <Button>Component notes</Button>;
componentNotes.story = {};
componentNotes.story.name = 'component notes';
componentNotes.story.parameters = { mdxSource: '<Button>Component notes</Button>' };
const componentMeta = { title: 'Button', id: 'button-id', includeStories: ['componentNotes'] };
const mdxStoryNameToId = { 'component notes': 'button-id--component-notes' };
componentMeta.parameters = componentMeta.parameters || {};
componentMeta.parameters.docs = {
container: ({ context, children }) => (
<DocsContainer context={{ ...context, mdxStoryNameToId }}>{children}</DocsContainer>
),
page: MDXContent,
};
export default componentMeta;
"
`;

View File

@ -0,0 +1,88 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs-mdx-compiler-plugin decorators.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Button } from '@storybook/react/demo';
import { Story, Meta } from '@storybook/addon-docs/blocks';
const makeShortcode = name =>
function MDXDefaultShortcode(props) {
console.warn(
'Component ' +
name +
' was not imported, exported, or provided by MDXProvider as global scope'
);
return <div {...props} />;
};
const layoutProps = {};
const MDXLayout = 'wrapper';
function MDXContent({ components, ...props }) {
return (
<MDXLayout {...layoutProps} {...props} components={components} mdxType=\\"MDXLayout\\">
<Meta
title=\\"Button\\"
decorators={[
storyFn => (
<div
style={{
backgroundColor: 'yellow',
}}
>
{storyFn()}
</div>
),
]}
mdxType=\\"Meta\\"
/>
<h1>{\`Decorated story\`}</h1>
<Story
name=\\"one\\"
decorators={[storyFn => <div className=\\"local\\">{storyFn()}</div>]}
mdxType=\\"Story\\"
>
<Button mdxType=\\"Button\\">One</Button>
</Story>
</MDXLayout>
);
}
MDXContent.isMDXComponent = true;
export const one = () => <Button>One</Button>;
one.story = {};
one.story.name = 'one';
one.story.parameters = { mdxSource: '<Button>One</Button>' };
one.story.decorators = [storyFn => <div className=\\"local\\">{storyFn()}</div>];
const componentMeta = {
title: 'Button',
decorators: [
storyFn => (
<div
style={{
backgroundColor: 'yellow',
}}
>
{storyFn()}
</div>
),
],
includeStories: ['one'],
};
const mdxStoryNameToId = { one: 'button--one' };
componentMeta.parameters = componentMeta.parameters || {};
componentMeta.parameters.docs = {
container: ({ context, children }) => (
<DocsContainer context={{ ...context, mdxStoryNameToId }}>{children}</DocsContainer>
),
page: MDXContent,
};
export default componentMeta;
"
`;

View File

@ -0,0 +1,57 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs-mdx-compiler-plugin docs-only.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Meta } from '@storybook/addon-docs/blocks';
const makeShortcode = name =>
function MDXDefaultShortcode(props) {
console.warn(
'Component ' +
name +
' was not imported, exported, or provided by MDXProvider as global scope'
);
return <div {...props} />;
};
const layoutProps = {};
const MDXLayout = 'wrapper';
function MDXContent({ components, ...props }) {
return (
<MDXLayout {...layoutProps} {...props} components={components} mdxType=\\"MDXLayout\\">
<Meta title=\\"docs-only\\" mdxType=\\"Meta\\" />
<h1>{\`Documentation only\`}</h1>
<p>
{\`This is a documentation-only MDX file which generates a dummy \`}
<inlineCode parentName=\\"p\\">{\`docsOnly: true\`}</inlineCode>
{\` story.\`}
</p>
</MDXLayout>
);
}
MDXContent.isMDXComponent = true;
export const __page = () => {
throw new Error('Docs-only story');
};
__page.story = { parameters: { docsOnly: true } };
const componentMeta = { title: 'docs-only', includeStories: ['__page'] };
const mdxStoryNameToId = {};
componentMeta.parameters = componentMeta.parameters || {};
componentMeta.parameters.docs = {
container: ({ context, children }) => (
<DocsContainer context={{ ...context, mdxStoryNameToId }}>{children}</DocsContainer>
),
page: MDXContent,
};
export default componentMeta;
"
`;

View File

@ -0,0 +1,66 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs-mdx-compiler-plugin non-story-exports.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Button } from '@storybook/react/demo';
import { Story, Meta } from '@storybook/addon-docs/blocks';
export const two = 2;
const makeShortcode = name =>
function MDXDefaultShortcode(props) {
console.warn(
'Component ' +
name +
' was not imported, exported, or provided by MDXProvider as global scope'
);
return <div {...props} />;
};
const layoutProps = {
two,
};
const MDXLayout = 'wrapper';
function MDXContent({ components, ...props }) {
return (
<MDXLayout {...layoutProps} {...props} components={components} mdxType=\\"MDXLayout\\">
<Meta title=\\"Button\\" mdxType=\\"Meta\\" />
<h1>{\`Story definition\`}</h1>
<Story name=\\"one\\" mdxType=\\"Story\\">
<Button mdxType=\\"Button\\">One</Button>
</Story>
<Story name=\\"hello story\\" mdxType=\\"Story\\">
<Button mdxType=\\"Button\\">Hello button</Button>
</Story>
</MDXLayout>
);
}
MDXContent.isMDXComponent = true;
export const one = () => <Button>One</Button>;
one.story = {};
one.story.name = 'one';
one.story.parameters = { mdxSource: '<Button>One</Button>' };
export const helloStory = () => <Button>Hello button</Button>;
helloStory.story = {};
helloStory.story.name = 'hello story';
helloStory.story.parameters = { mdxSource: '<Button>Hello button</Button>' };
const componentMeta = { title: 'Button', includeStories: ['one', 'helloStory'] };
const mdxStoryNameToId = { one: 'button--one', 'hello story': 'button--hello-story' };
componentMeta.parameters = componentMeta.parameters || {};
componentMeta.parameters.docs = {
container: ({ context, children }) => (
<DocsContainer context={{ ...context, mdxStoryNameToId }}>{children}</DocsContainer>
),
page: MDXContent,
};
export default componentMeta;
"
`;

View File

@ -0,0 +1,89 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs-mdx-compiler-plugin parameters.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Button } from '@storybook/react/demo';
import { Story, Meta } from '@storybook/addon-docs/blocks';
const makeShortcode = name =>
function MDXDefaultShortcode(props) {
console.warn(
'Component ' +
name +
' was not imported, exported, or provided by MDXProvider as global scope'
);
return <div {...props} />;
};
const layoutProps = {};
const MDXLayout = 'wrapper';
function MDXContent({ components, ...props }) {
return (
<MDXLayout {...layoutProps} {...props} components={components} mdxType=\\"MDXLayout\\">
<Meta
title=\\"Button\\"
component={Button}
parameters={{
notes: 'component notes',
}}
mdxType=\\"Meta\\"
/>
<Story name=\\"component notes\\" mdxType=\\"Story\\">
<Button mdxType=\\"Button\\">Component notes</Button>
</Story>
<Story
name=\\"story notes\\"
parameters={{
notes: 'story notes',
}}
mdxType=\\"Story\\"
>
<Button mdxType=\\"Button\\">Story notes</Button>
</Story>
</MDXLayout>
);
}
MDXContent.isMDXComponent = true;
export const componentNotes = () => <Button>Component notes</Button>;
componentNotes.story = {};
componentNotes.story.name = 'component notes';
componentNotes.story.parameters = { mdxSource: '<Button>Component notes</Button>' };
export const storyNotes = () => <Button>Story notes</Button>;
storyNotes.story = {};
storyNotes.story.name = 'story notes';
storyNotes.story.parameters = {
mdxSource: '<Button>Story notes</Button>',
...{
notes: 'story notes',
},
};
const componentMeta = {
title: 'Button',
parameters: {
notes: 'component notes',
},
includeStories: ['componentNotes', 'storyNotes'],
};
const mdxStoryNameToId = {
'component notes': 'button--component-notes',
'story notes': 'button--story-notes',
};
componentMeta.parameters = componentMeta.parameters || {};
componentMeta.parameters.docs = {
container: ({ context, children }) => (
<DocsContainer context={{ ...context, mdxStoryNameToId }}>{children}</DocsContainer>
),
page: MDXContent,
};
export default componentMeta;
"
`;

View File

@ -0,0 +1,85 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs-mdx-compiler-plugin previews.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Button } from '@storybook/react/demo';
import { Preview, Story, Meta } from '@storybook/addon-docs/blocks';
const makeShortcode = name =>
function MDXDefaultShortcode(props) {
console.warn(
'Component ' +
name +
' was not imported, exported, or provided by MDXProvider as global scope'
);
return <div {...props} />;
};
const layoutProps = {};
const MDXLayout = 'wrapper';
function MDXContent({ components, ...props }) {
return (
<MDXLayout {...layoutProps} {...props} components={components} mdxType=\\"MDXLayout\\">
<Meta
title=\\"Button\\"
component={Button}
parameters={{
notes: 'component notes',
}}
mdxType=\\"Meta\\"
/>
<h1>{\`Preview\`}</h1>
<p>{\`Previews can contain normal components, stories, and story references\`}</p>
<Preview mdxType=\\"Preview\\">
<Button mdxType=\\"Button\\">Just a button</Button>
<Story name=\\"hello button\\" mdxType=\\"Story\\">
<Button mdxType=\\"Button\\">Hello button</Button>
</Story>
<Story name=\\"two\\" mdxType=\\"Story\\">
<Button mdxType=\\"Button\\">Two</Button>
</Story>
<Story id=\\"welcome--welcome\\" mdxType=\\"Story\\" />
</Preview>
<p>{\`Preview wthout a story\`}</p>
<Preview mdxSource=\\"%0A%3CButton%3EJust%20a%20button%3C/Button%3E%0A\\" mdxType=\\"Preview\\">
<Button mdxType=\\"Button\\">Just a button</Button>
</Preview>
</MDXLayout>
);
}
MDXContent.isMDXComponent = true;
export const helloButton = () => <Button>Hello button</Button>;
helloButton.story = {};
helloButton.story.name = 'hello button';
helloButton.story.parameters = { mdxSource: '<Button>Hello button</Button>' };
export const two = () => <Button>Two</Button>;
two.story = {};
two.story.name = 'two';
two.story.parameters = { mdxSource: '<Button>Two</Button>' };
const componentMeta = {
title: 'Button',
parameters: {
notes: 'component notes',
},
includeStories: ['helloButton', 'two'],
};
const mdxStoryNameToId = { 'hello button': 'button--hello-button', two: 'button--two' };
componentMeta.parameters = componentMeta.parameters || {};
componentMeta.parameters.docs = {
container: ({ context, children }) => (
<DocsContainer context={{ ...context, mdxStoryNameToId }}>{children}</DocsContainer>
),
page: MDXContent,
};
export default componentMeta;
"
`;

View File

@ -0,0 +1,46 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs-mdx-compiler-plugin story-current.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Story } from '@storybook/addon-docs/blocks';
const makeShortcode = name =>
function MDXDefaultShortcode(props) {
console.warn(
'Component ' +
name +
' was not imported, exported, or provided by MDXProvider as global scope'
);
return <div {...props} />;
};
const layoutProps = {};
const MDXLayout = 'wrapper';
function MDXContent({ components, ...props }) {
return (
<MDXLayout {...layoutProps} {...props} components={components} mdxType=\\"MDXLayout\\">
<h1>{\`Current story\`}</h1>
<Story id=\\".\\" mdxType=\\"Story\\" />
</MDXLayout>
);
}
MDXContent.isMDXComponent = true;
const componentMeta = { includeStories: [] };
const mdxStoryNameToId = {};
componentMeta.parameters = componentMeta.parameters || {};
componentMeta.parameters.docs = {
container: ({ context, children }) => (
<DocsContainer context={{ ...context, mdxStoryNameToId }}>{children}</DocsContainer>
),
page: MDXContent,
};
export default componentMeta;
"
`;

View File

@ -0,0 +1,54 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs-mdx-compiler-plugin story-def-text-only.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Story, Meta } from '@storybook/addon-docs/blocks';
const makeShortcode = name =>
function MDXDefaultShortcode(props) {
console.warn(
'Component ' +
name +
' was not imported, exported, or provided by MDXProvider as global scope'
);
return <div {...props} />;
};
const layoutProps = {};
const MDXLayout = 'wrapper';
function MDXContent({ components, ...props }) {
return (
<MDXLayout {...layoutProps} {...props} components={components} mdxType=\\"MDXLayout\\">
<Meta title=\\"Text\\" mdxType=\\"Meta\\" />
<h1>{\`Story definition\`}</h1>
<Story name=\\"text\\" mdxType=\\"Story\\">
Plain text
</Story>
</MDXLayout>
);
}
MDXContent.isMDXComponent = true;
export const text = makeStoryFn('Plain text');
text.story = {};
text.story.name = 'text';
text.story.parameters = { mdxSource: \\"'Plain text'\\" };
const componentMeta = { title: 'Text', includeStories: ['text'] };
const mdxStoryNameToId = { text: 'text--text' };
componentMeta.parameters = componentMeta.parameters || {};
componentMeta.parameters.docs = {
container: ({ context, children }) => (
<DocsContainer context={{ ...context, mdxStoryNameToId }}>{children}</DocsContainer>
),
page: MDXContent,
};
export default componentMeta;
"
`;

View File

@ -0,0 +1,87 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs-mdx-compiler-plugin story-definitions.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Button } from '@storybook/react/demo';
import { Story, Meta } from '@storybook/addon-docs/blocks';
const makeShortcode = name =>
function MDXDefaultShortcode(props) {
console.warn(
'Component ' +
name +
' was not imported, exported, or provided by MDXProvider as global scope'
);
return <div {...props} />;
};
const layoutProps = {};
const MDXLayout = 'wrapper';
function MDXContent({ components, ...props }) {
return (
<MDXLayout {...layoutProps} {...props} components={components} mdxType=\\"MDXLayout\\">
<Meta title=\\"Button\\" mdxType=\\"Meta\\" />
<h1>{\`Story definition\`}</h1>
<Story name=\\"one\\" mdxType=\\"Story\\">
<Button mdxType=\\"Button\\">One</Button>
</Story>
<Story name=\\"hello story\\" mdxType=\\"Story\\">
<Button mdxType=\\"Button\\">Hello button</Button>
</Story>
<Story name=\\"w/punctuation\\" mdxType=\\"Story\\">
<Button mdxType=\\"Button\\">with punctuation</Button>
</Story>
<Story name=\\"1 fine day\\" mdxType=\\"Story\\">
<Button mdxType=\\"Button\\">starts with number</Button>
</Story>
</MDXLayout>
);
}
MDXContent.isMDXComponent = true;
export const one = () => <Button>One</Button>;
one.story = {};
one.story.name = 'one';
one.story.parameters = { mdxSource: '<Button>One</Button>' };
export const helloStory = () => <Button>Hello button</Button>;
helloStory.story = {};
helloStory.story.name = 'hello story';
helloStory.story.parameters = { mdxSource: '<Button>Hello button</Button>' };
export const wPunctuation = () => <Button>with punctuation</Button>;
wPunctuation.story = {};
wPunctuation.story.name = 'w/punctuation';
wPunctuation.story.parameters = { mdxSource: '<Button>with punctuation</Button>' };
export const _1FineDay = () => <Button>starts with number</Button>;
_1FineDay.story = {};
_1FineDay.story.name = '1 fine day';
_1FineDay.story.parameters = { mdxSource: '<Button>starts with number</Button>' };
const componentMeta = {
title: 'Button',
includeStories: ['one', 'helloStory', 'wPunctuation', '_1FineDay'],
};
const mdxStoryNameToId = {
one: 'button--one',
'hello story': 'button--hello-story',
'w/punctuation': 'button--w-punctuation',
'1 fine day': 'button--1-fine-day',
};
componentMeta.parameters = componentMeta.parameters || {};
componentMeta.parameters.docs = {
container: ({ context, children }) => (
<DocsContainer context={{ ...context, mdxStoryNameToId }}>{children}</DocsContainer>
),
page: MDXContent,
};
export default componentMeta;
"
`;

View File

@ -0,0 +1,58 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs-mdx-compiler-plugin story-function-var.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Meta, Story } from '@storybook/addon-docs/blocks';
export const basicFn = () => <Button mdxType=\\"Button\\" />;
const makeShortcode = name =>
function MDXDefaultShortcode(props) {
console.warn(
'Component ' +
name +
' was not imported, exported, or provided by MDXProvider as global scope'
);
return <div {...props} />;
};
const Button = makeShortcode('Button');
const layoutProps = {
basicFn,
};
const MDXLayout = 'wrapper';
function MDXContent({ components, ...props }) {
return (
<MDXLayout {...layoutProps} {...props} components={components} mdxType=\\"MDXLayout\\">
<Meta title=\\"story-function-var\\" mdxType=\\"Meta\\" />
<h1>{\`Button\`}</h1>
<p>{\`I can define a story with the function defined in CSF:\`}</p>
<Story name=\\"basic\\" mdxType=\\"Story\\">
{basicFn}
</Story>
</MDXLayout>
);
}
MDXContent.isMDXComponent = true;
export const basic = makeStoryFn(basicFn);
basic.story = {};
basic.story.name = 'basic';
basic.story.parameters = { mdxSource: 'basicFn' };
const componentMeta = { title: 'story-function-var', includeStories: ['basic'] };
const mdxStoryNameToId = { basic: 'story-function-var--basic' };
componentMeta.parameters = componentMeta.parameters || {};
componentMeta.parameters.docs = {
container: ({ context, children }) => (
<DocsContainer context={{ ...context, mdxStoryNameToId }}>{children}</DocsContainer>
),
page: MDXContent,
};
export default componentMeta;
"
`;

View File

@ -0,0 +1,63 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs-mdx-compiler-plugin story-function.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
const makeShortcode = name =>
function MDXDefaultShortcode(props) {
console.warn(
'Component ' +
name +
' was not imported, exported, or provided by MDXProvider as global scope'
);
return <div {...props} />;
};
const Story = makeShortcode('Story');
const layoutProps = {};
const MDXLayout = 'wrapper';
function MDXContent({ components, ...props }) {
return (
<MDXLayout {...layoutProps} {...props} components={components} mdxType=\\"MDXLayout\\">
<Story name=\\"function\\" height=\\"100px\\" mdxType=\\"Story\\">
{() => {
const btn = document.createElement('button');
btn.innerHTML = 'Hello Button';
btn.addEventListener('click', action('Click'));
return btn;
}}
</Story>
</MDXLayout>
);
}
MDXContent.isMDXComponent = true;
export const functionStory = makeStoryFn(() => {
const btn = document.createElement('button');
btn.innerHTML = 'Hello Button';
btn.addEventListener('click', action('Click'));
return btn;
});
functionStory.story = {};
functionStory.story.name = 'function';
functionStory.story.parameters = {
mdxSource:
\\"() => {\\\\n const btn = document.createElement('button');\\\\n btn.innerHTML = 'Hello Button';\\\\n btn.addEventListener('click', action('Click'));\\\\n return btn;\\\\n}\\",
};
const componentMeta = { includeStories: ['functionStory'] };
const mdxStoryNameToId = {};
componentMeta.parameters = componentMeta.parameters || {};
componentMeta.parameters.docs = {
container: ({ context, children }) => (
<DocsContainer context={{ ...context, mdxStoryNameToId }}>{children}</DocsContainer>
),
page: MDXContent,
};
export default componentMeta;
"
`;

View File

@ -0,0 +1,75 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs-mdx-compiler-plugin story-object.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Story, Meta } from '@storybook/addon-docs/blocks';
import { Welcome, Button } from '@storybook/angular/demo';
import { linkTo } from '@storybook/addon-links';
const makeShortcode = name =>
function MDXDefaultShortcode(props) {
console.warn(
'Component ' +
name +
' was not imported, exported, or provided by MDXProvider as global scope'
);
return <div {...props} />;
};
const layoutProps = {};
const MDXLayout = 'wrapper';
function MDXContent({ components, ...props }) {
return (
<MDXLayout {...layoutProps} {...props} components={components} mdxType=\\"MDXLayout\\">
<Meta title=\\"MDX|Welcome\\" mdxType=\\"Meta\\" />
<h1>{\`Story object\`}</h1>
<Story name=\\"to storybook\\" height=\\"300px\\" mdxType=\\"Story\\">
{{
template: \`<storybook-welcome-component (showApp)=\\"showApp()\\"></storybook-welcome-component>\`,
props: {
showApp: linkTo('Button'),
},
moduleMetadata: {
declarations: [Welcome],
},
}}
</Story>
</MDXLayout>
);
}
MDXContent.isMDXComponent = true;
export const toStorybook = makeStoryFn({
template: \`<storybook-welcome-component (showApp)=\\"showApp()\\"></storybook-welcome-component>\`,
props: {
showApp: linkTo('Button'),
},
moduleMetadata: {
declarations: [Welcome],
},
});
toStorybook.story = {};
toStorybook.story.name = 'to storybook';
toStorybook.story.parameters = {
mdxSource:
'{\\\\n template: \`<storybook-welcome-component (showApp)=\\"showApp()\\"></storybook-welcome-component>\`,\\\\n props: {\\\\n showApp: linkTo(\\\\'Button\\\\')\\\\n },\\\\n moduleMetadata: {\\\\n declarations: [Welcome]\\\\n }\\\\n}',
};
const componentMeta = { title: 'MDX|Welcome', includeStories: ['toStorybook'] };
const mdxStoryNameToId = { 'to storybook': 'mdx-welcome--to-storybook' };
componentMeta.parameters = componentMeta.parameters || {};
componentMeta.parameters.docs = {
container: ({ context, children }) => (
<DocsContainer context={{ ...context, mdxStoryNameToId }}>{children}</DocsContainer>
),
page: MDXContent,
};
export default componentMeta;
"
`;

View File

@ -0,0 +1,46 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs-mdx-compiler-plugin story-references.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Story } from '@storybook/addon-docs/blocks';
const makeShortcode = name =>
function MDXDefaultShortcode(props) {
console.warn(
'Component ' +
name +
' was not imported, exported, or provided by MDXProvider as global scope'
);
return <div {...props} />;
};
const layoutProps = {};
const MDXLayout = 'wrapper';
function MDXContent({ components, ...props }) {
return (
<MDXLayout {...layoutProps} {...props} components={components} mdxType=\\"MDXLayout\\">
<h1>{\`Story reference\`}</h1>
<Story id=\\"welcome--welcome\\" mdxType=\\"Story\\" />
</MDXLayout>
);
}
MDXContent.isMDXComponent = true;
const componentMeta = { includeStories: [] };
const mdxStoryNameToId = {};
componentMeta.parameters = componentMeta.parameters || {};
componentMeta.parameters.docs = {
container: ({ context, children }) => (
<DocsContainer context={{ ...context, mdxStoryNameToId }}>{children}</DocsContainer>
),
page: MDXContent,
};
export default componentMeta;
"
`;

Some files were not shown because too many files have changed in this diff Show More