Skip to content

Commit

Permalink
MDX: Better ergonomics for documenting CSF (#8312)
Browse files Browse the repository at this point in the history
MDX: Better ergonomics for documenting CSF
  • Loading branch information
shilman authored Oct 8, 2019
2 parents 252feb2 + ba753a1 commit 7d0123b
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 47 deletions.
58 changes: 31 additions & 27 deletions addons/docs/docs/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,46 +32,50 @@ The only limitation is that your exported titles (CSF: `default.title`, MDX `Met

Perhaps you want to write your stories in CSF, but document them in MDX? Here's how to do that:

**Button.mdx**

```md
import { Story } from '@storybook/addon-docs/blocks';
import { SomeComponent } from 'somewhere';

# Button

I can embed a story (but not define one, since this file should not contain a `Meta`):

<Story id="some--id" />

And of course I can also embed arbitrary markdown & JSX in this file.

<SomeComponent prop1="val1" />
```

**Button.stories.js**

```js
import React from 'react';
import { Button } from './Button';
import mdx from './Button.mdx';

export default {
title: 'Demo/Button',
parameters: {
docs: {
page: mdx,
},
},
component: Button,
includeStories: [], // or simply don't load this file at all
};

export const basic = () => <Button>Basic</Button>;
basic.story = {
parameters: { foo: 'bar' },
};
```

**Button.stories.mdx**

```md
import { Meta, Story } from '@storybook/addon-docs/blocks';
import * as stories from './Button.stories.js';
import { SomeComponent } from 'path/to/SomeComponent';

<Meta {...stories.default} />

# Button

I can define a story with the function imported from CSF:

<Story name="basic">{stories.basic}</Story>

And of course I can also embed arbitrary markdown & JSX in this file.

<SomeComponent prop1="val1" />
```

Note that in contrast to other examples, the MDX file suffix is `.mdx` rather than `.stories.mdx`. This key difference means that the file will be loaded with the default MDX loader rather than Storybook's CSF loader, which has several implications:
What's happening here:

1. You don't need to provide a `Meta` declaration.
2. You can refer to existing stories (i.e. `<Story id="...">`) but cannot define new stories (i.e. `<Story name="...">`).
3. The documentation gets exported as the default export (MDX default) rather than as a parameter hanging off the default export (CSF).
- Your stories are defined in CSF, but because of `includeStories: []`, they are not actually added to Storybook.
- The MDX file is adding the stories to Storybook, and using the story function defined in CSF.
- The MDX loader is using story metadata from CSF, such as name, decorators, parameters, but will give giving preference to anything defined in the MDX file.
- The MDX file is using the Meta `default` defined in the CSF.

## Mixing storiesOf with CSF/MDX

Expand Down
3 changes: 3 additions & 0 deletions addons/docs/src/blocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ export * from './Props';
export * from './Source';
export * from './Story';
export * from './Wrapper';

// helper function for MDX
export const makeStoryFn = (val: any) => (typeof val === 'function' ? val : () => val);
89 changes: 73 additions & 16 deletions addons/docs/src/mdx/__snapshots__/mdx-compiler-plugin.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

exports[`docs-mdx-compiler-plugin decorators.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer } from '@storybook/addon-docs/blocks';
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Button } from '@storybook/react/demo';
import { Story, Meta } from '@storybook/addon-docs/blocks';
Expand Down Expand Up @@ -89,7 +89,7 @@ export default componentMeta;
exports[`docs-mdx-compiler-plugin docs-only.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer } from '@storybook/addon-docs/blocks';
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Meta } from '@storybook/addon-docs/blocks';
Expand Down Expand Up @@ -145,7 +145,7 @@ export default componentMeta;
exports[`docs-mdx-compiler-plugin non-story-exports.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer } from '@storybook/addon-docs/blocks';
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Button } from '@storybook/react/demo';
import { Story, Meta } from '@storybook/addon-docs/blocks';
Expand Down Expand Up @@ -210,7 +210,7 @@ export default componentMeta;
exports[`docs-mdx-compiler-plugin parameters.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer } from '@storybook/addon-docs/blocks';
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Button } from '@storybook/react/demo';
import { Story, Meta } from '@storybook/addon-docs/blocks';
Expand Down Expand Up @@ -298,7 +298,7 @@ export default componentMeta;
exports[`docs-mdx-compiler-plugin previews.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer } from '@storybook/addon-docs/blocks';
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Button } from '@storybook/react/demo';
import { Preview, Story, Meta } from '@storybook/addon-docs/blocks';
Expand Down Expand Up @@ -378,7 +378,7 @@ export default componentMeta;
exports[`docs-mdx-compiler-plugin story-current.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer } from '@storybook/addon-docs/blocks';
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Story } from '@storybook/addon-docs/blocks';
Expand Down Expand Up @@ -423,7 +423,7 @@ export default componentMeta;
exports[`docs-mdx-compiler-plugin story-def-text-only.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer } from '@storybook/addon-docs/blocks';
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Story, Meta } from '@storybook/addon-docs/blocks';
Expand Down Expand Up @@ -453,7 +453,7 @@ function MDXContent({ components, ...props }) {
MDXContent.isMDXComponent = true;
export const text = () => 'Plain text';
export const text = makeStoryFn('Plain text');
text.story = {};
text.story.name = 'text';
text.story.parameters = { mdxSource: \\"'Plain text'\\" };
Expand All @@ -476,7 +476,7 @@ export default componentMeta;
exports[`docs-mdx-compiler-plugin story-definitions.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer } from '@storybook/addon-docs/blocks';
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Button } from '@storybook/react/demo';
import { Story, Meta } from '@storybook/addon-docs/blocks';
Expand Down Expand Up @@ -550,7 +550,7 @@ export default componentMeta;
exports[`docs-mdx-compiler-plugin story-function.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer } from '@storybook/addon-docs/blocks';
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
const makeShortcode = name =>
function MDXDefaultShortcode(props) {
Expand Down Expand Up @@ -581,12 +581,12 @@ function MDXContent({ components, ...props }) {
MDXContent.isMDXComponent = true;
export const functionStory = () => {
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 = {
Expand All @@ -610,9 +610,66 @@ 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 } from '@storybook/addon-docs/blocks';
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Story, Meta } from '@storybook/addon-docs/blocks';
import { Welcome, Button } from '@storybook/angular/demo';
Expand Down Expand Up @@ -652,7 +709,7 @@ function MDXContent({ components, ...props }) {
MDXContent.isMDXComponent = true;
export const toStorybook = () => ({
export const toStorybook = makeStoryFn({
template: \`<storybook-welcome-component (showApp)=\\"showApp()\\"></storybook-welcome-component>\`,
props: {
showApp: linkTo('Button'),
Expand Down Expand Up @@ -686,7 +743,7 @@ export default componentMeta;
exports[`docs-mdx-compiler-plugin story-references.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer } from '@storybook/addon-docs/blocks';
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Story } from '@storybook/addon-docs/blocks';
Expand Down Expand Up @@ -731,7 +788,7 @@ export default componentMeta;
exports[`docs-mdx-compiler-plugin vanilla.mdx 1`] = `
"/* @jsx mdx */
import { DocsContainer } from '@storybook/addon-docs/blocks';
import { DocsContainer, makeStoryFn } from '@storybook/addon-docs/blocks';
import { Button } from '@storybook/react/demo';
Expand Down
11 changes: 11 additions & 0 deletions addons/docs/src/mdx/__testfixtures__/story-function-var.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Meta, Story } from '@storybook/addon-docs/blocks';

<Meta title="story-function-var" />

export const basicFn = () => <Button />;

# Button

I can define a story with the function defined in CSF:

<Story name="basic">{basicFn}</Story>
11 changes: 7 additions & 4 deletions addons/docs/src/mdx/mdx-compiler-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ function genStoryExport(ast, context) {

let body = ast.children.find(n => n.type !== 'JSXText');
let storyCode = null;
let isJsx = false;
if (!body) {
// plain text node
const { code } = generate(ast.children[0], {});
Expand All @@ -60,18 +61,20 @@ function genStoryExport(ast, context) {
if (body.type === 'JSXExpressionContainer') {
// FIXME: handle fragments
body = body.expression;
} else {
isJsx = true;
}
const { code } = generate(body, {});
storyCode = code;
}
if (storyCode.trim().startsWith('() =>')) {
statements.push(`export const ${storyKey} = ${storyCode}`);
} else {
if (isJsx) {
statements.push(
`export const ${storyKey} = () => (
${storyCode}
);`
);
} else {
statements.push(`export const ${storyKey} = makeStoryFn(${storyCode});`);
}
statements.push(`${storyKey}.story = {};`);

Expand Down Expand Up @@ -240,7 +243,7 @@ function extractExports(node, options) {
);

const fullJsx = [
'import { DocsContainer } from "@storybook/addon-docs/blocks";',
'import { DocsContainer, makeStoryFn } from "@storybook/addon-docs/blocks";',
defaultJsx,
...storyExports,
`const componentMeta = ${stringifyMeta(metaExport)};`,
Expand Down
1 change: 1 addition & 0 deletions addons/docs/src/mdx/mdx-compiler-plugin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ describe('docs-mdx-compiler-plugin', () => {
'non-story-exports.mdx',
'story-function.mdx',
'docs-only.mdx',
'story-function-var.mdx',
];
fixtures.forEach(fixtureFile => {
it(fixtureFile, async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react';
import { Button } from '@storybook/react/demo';

export default {
title: 'Addons|Docs/csf-with-mdx-docs',
component: Button,
includeStories: [], // or simply don't load this file at all
};

// eslint-disable-next-line react/prop-types
export const basic = ({ parameters }) => <Button>Basic</Button>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Meta, Story } from '@storybook/addon-docs/blocks';
import * as stories from './csf-with-mdx-docs.stories';

<Meta title="Addons|Docs/csf-with-mdx-docs" />

# Button

I can define a story with the function imported from CSF:

<Story name="basic">{stories.basic}</Story>

1 comment on commit 7d0123b

@vercel
Copy link

@vercel vercel bot commented on 7d0123b Oct 8, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.