mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-07 23:22:10 +08:00
Merge branch 'next' into add-angular-fixtures
This commit is contained in:
commit
3760140fcd
@ -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',
|
||||
|
42
.eslintrc.js
42
.eslintrc.js
@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
145
CHANGELOG.md
145
CHANGELOG.md
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
31
MIGRATION.md
31
MIGRATION.md
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
|
@ -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', () => {
|
||||
|
@ -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;
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -185,7 +185,4 @@ class HighlightToggle extends Component<ToggleProps> {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(HighlightToggle);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(HighlightToggle);
|
||||
|
@ -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 */",
|
||||
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
@ -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 };
|
||||
|
@ -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));
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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',
|
||||
},
|
||||
];
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
},
|
||||
{
|
||||
|
@ -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',
|
||||
},
|
||||
{
|
||||
|
@ -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"
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
1
addons/docs/angular/config.js
vendored
1
addons/docs/angular/config.js
vendored
@ -1 +0,0 @@
|
||||
module.exports = require('../dist/frameworks/angular/config');
|
@ -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
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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",
|
||||
|
39
addons/docs/postinstall/presets.js
Normal file
39
addons/docs/postinstall/presets.js
Normal 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
1
addons/docs/preset.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require('./dist/preset');
|
@ -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 };
|
||||
|
18
addons/docs/src/blocks/DocsPage.test.ts
Normal file
18
addons/docs/src/blocks/DocsPage.test.ts
Normal 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');
|
||||
});
|
||||
});
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
|
25
addons/docs/src/blocks/DocsStory.tsx
Normal file
25
addons/docs/src/blocks/DocsStory.tsx
Normal 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>
|
||||
);
|
7
addons/docs/src/blocks/Heading.tsx
Normal file
7
addons/docs/src/blocks/Heading.tsx
Normal 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>;
|
16
addons/docs/src/blocks/Primary.tsx
Normal file
16
addons/docs/src/blocks/Primary.tsx
Normal 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;
|
||||
};
|
@ -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 };
|
||||
|
33
addons/docs/src/blocks/Stories.tsx
Normal file
33
addons/docs/src/blocks/Stories.tsx
Normal 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',
|
||||
};
|
7
addons/docs/src/blocks/Subheading.tsx
Normal file
7
addons/docs/src/blocks/Subheading.tsx
Normal 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>;
|
19
addons/docs/src/blocks/Subtitle.tsx
Normal file
19
addons/docs/src/blocks/Subtitle.tsx
Normal 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;
|
||||
};
|
48
addons/docs/src/blocks/Title.tsx
Normal file
48
addons/docs/src/blocks/Title.tsx
Normal 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;
|
||||
};
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
34
addons/docs/src/blocks/utils.ts
Normal file
34
addons/docs/src/blocks/utils.ts
Normal 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;
|
||||
};
|
@ -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);
|
||||
|
@ -1 +1 @@
|
||||
export * from '../../lib/docgenUtils';
|
||||
export * from '../../lib/docgen';
|
||||
|
@ -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;
|
||||
|
@ -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];
|
||||
}
|
||||
|
@ -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: {
|
||||
|
@ -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'),
|
||||
|
199
addons/docs/src/frameworks/react/inspection/acornParser.test.ts
Normal file
199
addons/docs/src/frameworks/react/inspection/acornParser.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
211
addons/docs/src/frameworks/react/inspection/acornParser.ts
Normal file
211
addons/docs/src/frameworks/react/inspection/acornParser.ts
Normal 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;
|
||||
}
|
14
addons/docs/src/frameworks/react/inspection/inspectValue.ts
Normal file
14
addons/docs/src/frameworks/react/inspection/inspectValue.ts
Normal 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 } };
|
||||
}
|
79
addons/docs/src/frameworks/react/inspection/types.ts
Normal file
79
addons/docs/src/frameworks/react/inspection/types.ts
Normal 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;
|
||||
}
|
6
addons/docs/src/frameworks/react/propTypes/captions.ts
Normal file
6
addons/docs/src/frameworks/react/propTypes/captions.ts
Normal 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';
|
115
addons/docs/src/frameworks/react/propTypes/createDefaultValue.ts
Normal file
115
addons/docs/src/frameworks/react/propTypes/createDefaultValue.ts
Normal 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;
|
||||
}
|
||||
}
|
366
addons/docs/src/frameworks/react/propTypes/createType.ts
Normal file
366
addons/docs/src/frameworks/react/propTypes/createType.ts
Normal 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;
|
||||
}
|
||||
}
|
25
addons/docs/src/frameworks/react/propTypes/generateCode.ts
Normal file
25
addons/docs/src/frameworks/react/propTypes/generateCode.ts
Normal 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);
|
||||
}
|
@ -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');
|
||||
});
|
@ -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(' ');
|
||||
}
|
955
addons/docs/src/frameworks/react/propTypes/handleProp.test.ts
Normal file
955
addons/docs/src/frameworks/react/propTypes/handleProp.test.ts
Normal 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');
|
||||
});
|
||||
});
|
35
addons/docs/src/frameworks/react/propTypes/handleProp.ts
Normal file
35
addons/docs/src/frameworks/react/propTypes/handleProp.ts
Normal 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);
|
||||
}
|
5
addons/docs/src/frameworks/react/propTypes/isHtmlTag.ts
Normal file
5
addons/docs/src/frameworks/react/propTypes/isHtmlTag.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import htmlTags from 'html-tags';
|
||||
|
||||
export function isHtmlTag(tagName: string): boolean {
|
||||
return htmlTags.includes(tagName.toLowerCase());
|
||||
}
|
21
addons/docs/src/frameworks/react/propTypes/sortProps.ts
Normal file
21
addons/docs/src/frameworks/react/propTypes/sortProps.ts
Normal 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;
|
||||
}
|
@ -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: {
|
||||
|
@ -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 };
|
||||
};
|
||||
|
8
addons/docs/src/frameworks/vue/preset.ts
Normal file
8
addons/docs/src/frameworks/vue/preset.ts
Normal 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;
|
||||
}
|
@ -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 },
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
23
addons/docs/src/lib/docgen/createDefaultValue.ts
Normal file
23
addons/docs/src/lib/docgen/createDefaultValue.ts
Normal 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;
|
||||
}
|
99
addons/docs/src/lib/docgen/createPropDef.ts
Normal file
99
addons/docs/src/lib/docgen/createPropDef.ts
Normal 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;
|
||||
}
|
||||
};
|
155
addons/docs/src/lib/docgen/extractDocgenProps.test.ts
Normal file
155
addons/docs/src/lib/docgen/extractDocgenProps.test.ts
Normal 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();
|
||||
});
|
||||
});
|
77
addons/docs/src/lib/docgen/extractDocgenProps.ts
Normal file
77
addons/docs/src/lib/docgen/extractDocgenProps.ts
Normal 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;
|
||||
}
|
3
addons/docs/src/lib/docgen/index.ts
Normal file
3
addons/docs/src/lib/docgen/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './types';
|
||||
export * from './utils';
|
||||
export * from './extractDocgenProps';
|
44
addons/docs/src/lib/docgen/types.ts
Normal file
44
addons/docs/src/lib/docgen/types.ts
Normal 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',
|
||||
}
|
29
addons/docs/src/lib/docgen/utils.ts
Normal file
29
addons/docs/src/lib/docgen/utils.ts
Normal 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);
|
@ -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);
|
@ -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;
|
||||
}
|
344
addons/docs/src/lib/jsdocParser.test.ts
Normal file
344
addons/docs/src/lib/jsdocParser.test.ts
Normal 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');
|
||||
});
|
||||
});
|
234
addons/docs/src/lib/jsdocParser.ts
Normal file
234
addons/docs/src/lib/jsdocParser.ts
Normal 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;
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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];
|
||||
};
|
@ -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;
|
||||
"
|
||||
`;
|
8
addons/docs/src/mdx/__testfixtures__/component-id.mdx
Normal file
8
addons/docs/src/mdx/__testfixtures__/component-id.mdx
Normal 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>
|
@ -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;
|
||||
"
|
||||
`;
|
@ -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;
|
||||
"
|
||||
`;
|
@ -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;
|
||||
"
|
||||
`;
|
@ -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;
|
||||
"
|
||||
`;
|
@ -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;
|
||||
"
|
||||
`;
|
@ -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;
|
||||
"
|
||||
`;
|
@ -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;
|
||||
"
|
||||
`;
|
@ -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;
|
||||
"
|
||||
`;
|
@ -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;
|
||||
"
|
||||
`;
|
@ -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;
|
||||
"
|
||||
`;
|
@ -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;
|
||||
"
|
||||
`;
|
@ -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;
|
||||
"
|
||||
`;
|
@ -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
Loading…
x
Reference in New Issue
Block a user