Skip to content

Commit

Permalink
Addon-docs: Support non-story exports in MDX (#7188)
Browse files Browse the repository at this point in the history
Addon-docs: Support non-story exports in MDX
  • Loading branch information
shilman authored Jun 25, 2019
2 parents 4bc51e3 + e5d33fb commit ba5ef30
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 25 deletions.
74 changes: 68 additions & 6 deletions addons/docs/__snapshots__/mdx-compiler-plugin.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function MDXContent({ components, ...props }) {
MDXContent.isMDXComponent = true;
const componentMeta = {};
const componentMeta = { includeStories: [] };
const mdxKind = componentMeta.title || componentMeta.displayName;
const WrappedMDXContent = ({ context }) => (
Expand Down Expand Up @@ -105,6 +105,7 @@ const componentMeta = {
</div>
),
],
includeStories: ['one'],
};
const mdxKind = componentMeta.title || componentMeta.displayName;
Expand All @@ -118,6 +119,65 @@ export default componentMeta;
"
`;
exports[`docs-mdx-compiler-plugin supports non-story exports 1`] = `
"/* @jsx mdx */
import { DocsContainer } 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.parameters = { mdxSource: \`<Button>One</Button>\` };
export const helloStory = () => <Button>Hello button</Button>;
helloStory.title = 'hello story';
helloStory.parameters = { mdxSource: \`<Button>Hello button</Button>\` };
const componentMeta = { title: 'Button', includeStories: ['one', 'helloStory'] };
const mdxKind = componentMeta.title || componentMeta.displayName;
const WrappedMDXContent = ({ context }) => (
<DocsContainer context={{ ...context, mdxKind }} content={MDXContent} />
);
componentMeta.parameters = componentMeta.parameters || {};
componentMeta.parameters.docs = WrappedMDXContent;
export default componentMeta;
"
`;
exports[`docs-mdx-compiler-plugin supports object-style story definitions 1`] = `
"/* @jsx mdx */
import { DocsContainer } from '@storybook/addon-docs/blocks';
Expand Down Expand Up @@ -182,7 +242,7 @@ toStorybook.parameters = {
}\`,
};
const componentMeta = { title: 'MDX|Welcome' };
const componentMeta = { title: 'MDX|Welcome', includeStories: ['toStorybook'] };
const mdxKind = componentMeta.title || componentMeta.displayName;
const WrappedMDXContent = ({ context }) => (
Expand Down Expand Up @@ -262,6 +322,7 @@ const componentMeta = {
component: Button,
notes: 'component notes',
},
includeStories: ['componentNotes', 'storyNotes'],
};
const mdxKind = componentMeta.title || componentMeta.displayName;
Expand Down Expand Up @@ -336,6 +397,7 @@ const componentMeta = {
component: Button,
notes: 'component notes',
},
includeStories: ['helloButton', 'two'],
};
const mdxKind = componentMeta.title || componentMeta.displayName;
Expand Down Expand Up @@ -392,7 +454,7 @@ export const helloStory = () => <Button>Hello button</Button>;
helloStory.title = 'hello story';
helloStory.parameters = { mdxSource: \`<Button>Hello button</Button>\` };
const componentMeta = { title: 'Button' };
const componentMeta = { title: 'Button', includeStories: ['one', 'helloStory'] };
const mdxKind = componentMeta.title || componentMeta.displayName;
const WrappedMDXContent = ({ context }) => (
Expand Down Expand Up @@ -434,7 +496,7 @@ function MDXContent({ components, ...props }) {
MDXContent.isMDXComponent = true;
const componentMeta = {};
const componentMeta = { includeStories: [] };
const mdxKind = componentMeta.title || componentMeta.displayName;
const WrappedMDXContent = ({ context }) => (
Expand Down Expand Up @@ -482,7 +544,7 @@ MDXContent.isMDXComponent = true;
export const text = () => 'Plain text';
text.parameters = { mdxSource: \`'Plain text'\` };
const componentMeta = { title: 'Text' };
const componentMeta = { title: 'Text', includeStories: ['text'] };
const mdxKind = componentMeta.title || componentMeta.displayName;
const WrappedMDXContent = ({ context }) => (
Expand Down Expand Up @@ -525,7 +587,7 @@ function MDXContent({ components, ...props }) {
MDXContent.isMDXComponent = true;
const componentMeta = {};
const componentMeta = { includeStories: [] };
const mdxKind = componentMeta.title || componentMeta.displayName;
const WrappedMDXContent = ({ context }) => (
Expand Down
16 changes: 16 additions & 0 deletions addons/docs/fixtures/non-story-exports.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Button } from '@storybook/react/demo';
import { Story, Meta } from '@storybook/addon-docs/blocks';

<Meta title="Button" />

# Story definition

<Story name="one">
<Button>One</Button>
</Story>

export const two = 2;

<Story name="hello story">
<Button>Hello button</Button>
</Story>
58 changes: 39 additions & 19 deletions addons/docs/mdx-compiler-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ function genStoryExport(ast, counter) {
// console.log('genStoryExport', JSON.stringify(ast, null, 2));

const statements = [];
const storyFn = getStoryFn(storyName, counter);
const storyKey = getStoryFn(storyName, counter);

let body = ast.children.find(n => n.type !== 'JSXText');
let storyCode = null;
Expand All @@ -61,13 +61,13 @@ function genStoryExport(ast, counter) {
storyCode = code;
}
statements.push(
`export const ${storyFn} = () => (
`export const ${storyKey} = () => (
${storyCode}
);`
);

if (storyName !== storyFn) {
statements.push(`${storyFn}.title = '${storyName}';`);
if (storyName !== storyKey) {
statements.push(`${storyKey}.title = '${storyName}';`);
}

let parameters = getAttr(ast.openingElement, 'parameters');
Expand All @@ -76,48 +76,54 @@ function genStoryExport(ast, counter) {
if (parameters) {
const { code: params } = generate(parameters, {});
// FIXME: hack in the story's source as a parameter
statements.push(`${storyFn}.parameters = { mdxSource: ${source}, ...${params} };`);
statements.push(`${storyKey}.parameters = { mdxSource: ${source}, ...${params} };`);
} else {
statements.push(`${storyFn}.parameters = { mdxSource: ${source} };`);
statements.push(`${storyKey}.parameters = { mdxSource: ${source} };`);
}

// console.log(statements);

return [statements.join('\n')];
return {
[storyKey]: statements.join('\n'),
};
}

function genPreviewExports(ast, counter) {
// console.log('genPreviewExports', JSON.stringify(ast, null, 2));

let localCounter = counter;
const previewExports = [];
const previewExports = {};
for (let i = 0; i < ast.children.length; i += 1) {
const child = ast.children[i];
if (child.type === 'JSXElement' && child.openingElement.name.name === 'Story') {
const storyExport = genStoryExport(child, localCounter);
if (storyExport) {
previewExports.push(storyExport);
Object.assign(previewExports, storyExport);
localCounter += 1;
}
}
}
return previewExports;
}

function genMetaExport(ast) {
function genMeta(ast) {
let title = getAttr(ast.openingElement, 'title');
let parameters = getAttr(ast.openingElement, 'parameters');
let decorators = getAttr(ast.openingElement, 'decorators');
title = title && `title: '${title.value}',`;
title = title && `'${title.value}'`;
if (parameters && parameters.expression) {
const { code: params } = generate(parameters.expression, {});
parameters = `parameters: ${params},`;
parameters = params;
}
if (decorators && decorators.expression) {
const { code: decos } = generate(decorators.expression, {});
decorators = `decorators: ${decos},`;
decorators = decos;
}
return `const componentMeta = { ${title || ''} ${parameters || ''} ${decorators || ''} };`;
return {
title,
parameters,
decorators,
};
}

function getExports(node, counter) {
Expand All @@ -127,7 +133,7 @@ function getExports(node, counter) {
// Single story
const ast = parser.parseExpression(value, { plugins: ['jsx'] });
const storyExport = genStoryExport(ast, counter);
return storyExport && { stories: [storyExport] };
return storyExport && { stories: storyExport };
}
if (PREVIEW_REGEX.exec(value)) {
// Preview, possibly containing multiple stories
Expand All @@ -137,7 +143,7 @@ function getExports(node, counter) {
if (META_REGEX.exec(value)) {
// Preview, possibly containing multiple stories
const ast = parser.parseExpression(value, { plugins: ['jsx'] });
return { meta: genMetaExport(ast) };
return { meta: genMeta(ast) };
}
}
return null;
Expand All @@ -152,18 +158,31 @@ componentMeta.parameters = componentMeta.parameters || {};
componentMeta.parameters.docs = WrappedMDXContent;
`.trim();

function stringifyMeta(meta) {
let result = '{ ';
Object.entries(meta).forEach(([key, val]) => {
if (val) {
result += `${key}: ${val}, `;
}
});
result += ' }';
return result;
}

function extractExports(node, options) {
// we're overriding default export
const defaultJsx = mdxToJsx.toJSX(node, {}, { ...options, skipExport: true });
const storyExports = [];
const includeStories = [];
let metaExport = null;
let counter = 0;
node.children.forEach(n => {
const exports = getExports(n, counter);
if (exports) {
const { stories, meta } = exports;
if (stories) {
stories.forEach(story => {
Object.entries(stories).forEach(([key, story]) => {
includeStories.push(key);
storyExports.push(story);
counter += 1;
});
Expand All @@ -177,14 +196,15 @@ function extractExports(node, options) {
}
});
if (!metaExport) {
metaExport = 'const componentMeta = { };';
metaExport = {};
}
metaExport.includeStories = JSON.stringify(includeStories);

const fullJsx = [
'import { DocsContainer } from "@storybook/addon-docs/blocks";',
defaultJsx,
...storyExports,
metaExport,
`const componentMeta = ${stringifyMeta(metaExport)};`,
wrapperJs,
'export default componentMeta;',
].join('\n\n');
Expand Down
4 changes: 4 additions & 0 deletions addons/docs/mdx-compiler-plugin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ describe('docs-mdx-compiler-plugin', () => {
const code = await generate(path.resolve(__dirname, './fixtures/parameters.mdx'));
expect(code).toMatchSnapshot();
});
it('supports non-story exports', async () => {
const code = await generate(path.resolve(__dirname, './fixtures/non-story-exports.mdx'));
expect(code).toMatchSnapshot();
});
it('errors on missing story props', async () => {
await expect(
generate(path.resolve(__dirname, './fixtures/story-missing-props.mdx'))
Expand Down
8 changes: 8 additions & 0 deletions examples/official-storybook/stories/addon-docs.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ import { Button } from '@storybook/react/demo';

<Button>just a button, not a story</Button>

export const nonStory1 = 'foo'; // a non-story export

export const nonStory2 = () => <Button>Not a story</Button>; // another one

<Preview>
<Story name="hello story">
<Button onClick={action('clicked')}>hello world</Button>
Expand All @@ -67,6 +71,10 @@ import { Button } from '@storybook/react/demo';
<Story name="plaintext">Plain text</Story>
</Preview>

<Story name="solo story">
<Button onClick={action('clicked')}>solo</Button>
</Story>

<Source name="hello story" />

## Configurable height
Expand Down

0 comments on commit ba5ef30

Please sign in to comment.