Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MDX: Better ergonomics for documenting CSF #8312

Merged
merged 6 commits into from
Oct 8, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 31 additions & 27 deletions addons/docs/docs/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,46 +31,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
tmeasday marked this conversation as resolved.
Show resolved Hide resolved
};

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} />
tmeasday marked this conversation as resolved.
Show resolved Hide resolved

# 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>