Codemod: convert MDX back to module format

This commit is contained in:
Michael Shilman 2019-06-30 10:49:47 +08:00
parent 3974d99e15
commit b0cfb293b2
15 changed files with 390 additions and 12 deletions

View File

@ -4,7 +4,14 @@ const mdx = require('@mdx-js/mdx');
const prettier = require('prettier');
const plugin = require('./mdx-compiler-plugin');
function format(code) {
async function generate(filePath) {
const content = await fs.readFile(filePath, 'utf8');
const code = mdx.sync(content, {
filepath: filePath,
compilers: [plugin({})],
});
return prettier.format(code, {
parser: 'babel',
printWidth: 100,
@ -15,17 +22,6 @@ function format(code) {
});
}
async function generate(filePath) {
const content = await fs.readFile(filePath, 'utf8');
const result = mdx.sync(content, {
filepath: filePath,
compilers: [plugin({})],
});
return format(result);
}
describe('docs-mdx-compiler-plugin', () => {
it('supports vanilla mdx', async () => {
const code = await generate(path.resolve(__dirname, './fixtures/vanilla.mdx'));

View File

@ -189,6 +189,8 @@ This converts all of your component module stories into MDX format, which integr
./node_modules/.bin/jscodeshift -t ./node_modules/@storybook/codemod/dist/transforms/convert-to-module-format.js . --ignore-pattern "node_modules|dist"
```
For example:
```js
export default {
title: 'Button',
@ -215,3 +217,39 @@ import { Meta, Story } from '@storybook/addon-docs/blocks';
<Button label="Story 2" onClick={action('click')} />
</Story>
```
### convert-mdx-to-module
This converts all your MDX stories into module format.
```sh
./node_modules/.bin/jscodeshift -t ./node_modules/@storybook/codemod/dist/transforms/convert-to-module-format.js . --ignore-pattern "node_modules|dist" --extensions=mdx
```
For example:
```js
import React from 'react';
import Button from './Button';
import { Meta, Story } from '@storybook/addon-docs/blocks';
<Meta title='Button' />
<Story name='basic stories'><Button label='The Button' /></Story>
```
Becomes:
```
import React from 'react';
import Button from './Button';
export default {
title: 'Button',
};
export const basicStory = () => <Button label="The Button" />;
basicStory.story = {
name: 'basic stories',
};
```

View File

@ -21,6 +21,7 @@
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@mdx-js/mdx": "^1.0.0",
"core-js": "^3.0.1",
"jscodeshift": "^0.6.3",
"lodash": "^4.17.11",

View File

@ -0,0 +1,15 @@
import React from 'react';
import Button from './Button';
import { action } from '@storybook/addon-actions';
import { Meta, Story } from '@storybook/addon-docs/blocks';
<Meta title='Button' />
<Story name='story1'><Button label='Story 1' /></Story>
<Story name='second story'><Button label='Story 2' onClick={action('click')} /></Story>
<Story name='complex story'><div>
<Button label='The Button' onClick={action('onClick')} />
<br />
</div></Story>

View File

@ -0,0 +1,25 @@
import React from 'react';
import Button from './Button';
import { action } from '@storybook/addon-actions';
export default {
title: 'Button',
};
export const story1 = () => <Button label="Story 1" />;
export const secondStory = () => <Button label="Story 2" onClick={action('click')} />;
secondStory.story = {
name: 'second story',
};
export const complexStory = () => (
<div>
<Button label="The Button" onClick={action('onClick')} />
<br />
</div>
);
complexStory.story = {
name: 'complex story',
};

View File

@ -0,0 +1,9 @@
import React from 'react';
import Button from './Button';
import { Meta, Story } from '@storybook/addon-docs/blocks';
<Meta
title='Some.Button'
decorators={[withKnobs, storyFn => <div className='foo'>{storyFn}</div>]} />
<Story name='with decorator'><Button label='The Button' /></Story>

View File

@ -0,0 +1,13 @@
import React from 'react';
import Button from './Button';
export default {
title: 'Some.Button',
decorators: [withKnobs, storyFn => <div className="foo">{storyFn}</div>],
};
export const withDecorator = () => <Button label="The Button" />;
withDecorator.story = {
name: 'with decorator',
};

View File

@ -0,0 +1,20 @@
import React from 'react';
import Button from './Button';
import { action } from '@storybook/addon-actions';
import { Meta, Story } from '@storybook/addon-docs/blocks';
<Meta title='Button' />
export const rowData = {
col1: 'a',
col2: 2,
};
<Story name='story1'><Button label='Story 1' /></Story>
<Story name='second story'><Button label='Story 2' onClick={action('click')} /></Story>
<Story name='complex story'><div>
<Button label='The Button' onClick={action('onClick')} />
<br />
</div></Story>

View File

@ -0,0 +1,30 @@
import React from 'react';
import Button from './Button';
import { action } from '@storybook/addon-actions';
export const rowData = {
col1: 'a',
col2: 2,
};
export default {
title: 'Button',
includeStories: ['story1', 'secondStory', 'complexStory'],
};
export const story1 = () => <Button label="Story 1" />;
export const secondStory = () => <Button label="Story 2" onClick={action('click')} />;
secondStory.story = {
name: 'second story',
};
export const complexStory = () => (
<div>
<Button label="The Button" onClick={action('onClick')} />
<br />
</div>
);
complexStory.story = {
name: 'complex story',
};

View File

@ -0,0 +1,14 @@
import React from 'react';
import Button from './Button';
import { storiesOf } from '@storybook/react';
import { Meta, Story } from '@storybook/addon-docs/blocks';
<Meta
title='Button'
parameters={{
component: Button,
foo: 1,
bar: 2,
}} />
<Story name='with kind parameters'><Button label='The Button' /></Story>

View File

@ -0,0 +1,19 @@
import React from 'react';
import Button from './Button';
import { storiesOf } from '@storybook/react';
export default {
title: 'Button',
parameters: {
component: Button,
foo: 1,
bar: 2,
},
};
export const withKindParameters = () => <Button label="The Button" />;
withKindParameters.story = {
name: 'with kind parameters',
};

View File

@ -0,0 +1,19 @@
import React from 'react';
import Button from './Button';
import { storiesOf } from '@storybook/react';
import { Meta, Story } from '@storybook/addon-docs/blocks';
<Meta title='Button' />
<Story
name='with story parameters'
parameters={{
header: false,
inline: true,
}}><Button label='The Button' /></Story>
<Story
name='foo'
parameters={{
bar: 1,
}}><Button label='Foo' /></Story>

View File

@ -0,0 +1,26 @@
import React from 'react';
import Button from './Button';
import { storiesOf } from '@storybook/react';
export default {
title: 'Button',
};
export const withStoryParameters = () => <Button label="The Button" />;
withStoryParameters.story = {
name: 'with story parameters',
parameters: {
header: false,
inline: true,
},
};
export const foo = () => <Button label="Foo" />;
foo.story = {
parameters: {
bar: 1,
},
};

View File

@ -0,0 +1,7 @@
import { defineTest } from 'jscodeshift/dist/testUtils';
const testNames = ['basic', 'decorators', 'parameters', 'story-parameters', 'exclude-stories'];
testNames.forEach(testName => {
defineTest(__dirname, `convert-mdx-to-module`, null, `convert-mdx-to-module/${testName}`);
});

View File

@ -0,0 +1,146 @@
// import recast from 'recast';
import camelCase from 'lodash/camelCase';
import mdx from '@mdx-js/mdx';
import prettier from 'prettier';
/**
* Convert a compponent's MDX file into module story format
*/
export default function transformer(file, api) {
const j = api.jscodeshift;
const code = mdx.sync(file.source, {});
const root = j(code);
function parseJsxAttributes(attributes) {
const result = {};
attributes.forEach(attr => {
const key = attr.name.name;
const val = attr.value.type === 'JSXExpressionContainer' ? attr.value.expression : attr.value;
result[key] = val;
});
return result;
}
function genObjectExpression(attrs) {
return j.objectExpression(
Object.entries(attrs).map(([key, val]) => j.property('init', j.identifier(key), val))
);
}
function convertToStories(path) {
const base = j(path);
const meta = {};
const includeStories = [];
const storyStatements = [];
// get rid of all mdxType junk
base
.find(j.JSXAttribute)
.filter(attr => attr.node.name.name === 'mdxType')
.remove();
// parse <Meta title="..." />
base
.find(j.JSXElement)
.filter(elt => elt.node.openingElement.name.name === 'Meta')
.forEach(elt => {
const attrs = parseJsxAttributes(elt.node.openingElement.attributes);
Object.assign(meta, attrs);
});
// parse <Story name="..." />
base
.find(j.JSXElement)
.filter(elt => elt.node.openingElement.name.name === 'Story')
.forEach(elt => {
const attrs = parseJsxAttributes(elt.node.openingElement.attributes);
const storyKey = camelCase(attrs.name.value);
includeStories.push(storyKey);
if (storyKey === attrs.name.value) {
delete attrs.name;
}
storyStatements.push(
j.exportDeclaration(
false,
j.variableDeclaration('const', [
j.variableDeclarator(
j.identifier(storyKey),
j.arrowFunctionExpression([], elt.node.children[0])
),
])
)
);
if (Object.keys(attrs).length > 0) {
storyStatements.push(
j.assignmentStatement(
'=',
j.memberExpression(j.identifier(storyKey), j.identifier('story')),
genObjectExpression(attrs)
)
);
}
storyStatements.push(j.emptyStatement());
});
if (root.find(j.ExportNamedDeclaration).size() > 0) {
meta.includeStories = j.arrayExpression(includeStories.map(key => j.literal(key)));
}
const statements = [
j.exportDefaultDeclaration(genObjectExpression(meta)),
j.emptyStatement(),
...storyStatements,
];
const lastStatement = root.find(j.Statement).at(-1);
statements.reverse().forEach(stmt => {
lastStatement.insertAfter(stmt);
});
base.remove();
}
root.find(j.ExportDefaultDeclaration).forEach(convertToStories);
// strip out Story/Meta import and MDX junk
// /* @jsx mdx */
// const makeShortcode = ...
// const layoutProps = {};
// const MDXLayout = 'wrapper';
// MDXContent.isMDXComponent = true;
root
.find(j.ImportDeclaration)
.at(0)
.replaceWith(exp => j.importDeclaration(exp.node.specifiers, exp.node.source));
root
.find(j.ImportDeclaration)
.filter(exp => exp.node.source.value === '@storybook/addon-docs/blocks')
.remove();
const MDX_DECLS = ['makeShortcode', 'layoutProps', 'MDXLayout'];
root
.find(j.VariableDeclaration)
.filter(
decl =>
decl.node.declarations.length === 1 && MDX_DECLS.includes(decl.node.declarations[0].id.name)
)
.remove();
root
.find(j.AssignmentExpression)
.filter(
expr =>
expr.node.left.type === 'MemberExpression' && expr.node.left.object.name === 'MDXContent'
)
.remove();
const source = root.toSource({ trailingComma: true, quote: 'single', tabWidth: 2 });
return prettier.format(source, {
parser: 'babel',
printWidth: 100,
tabWidth: 2,
bracketSpacing: true,
trailingComma: 'es5',
singleQuote: true,
});
}