mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-07 07:21:17 +08:00
Codemod: convert MDX back to module format
This commit is contained in:
parent
3974d99e15
commit
b0cfb293b2
@ -4,7 +4,14 @@ const mdx = require('@mdx-js/mdx');
|
|||||||
const prettier = require('prettier');
|
const prettier = require('prettier');
|
||||||
const plugin = require('./mdx-compiler-plugin');
|
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, {
|
return prettier.format(code, {
|
||||||
parser: 'babel',
|
parser: 'babel',
|
||||||
printWidth: 100,
|
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', () => {
|
describe('docs-mdx-compiler-plugin', () => {
|
||||||
it('supports vanilla mdx', async () => {
|
it('supports vanilla mdx', async () => {
|
||||||
const code = await generate(path.resolve(__dirname, './fixtures/vanilla.mdx'));
|
const code = await generate(path.resolve(__dirname, './fixtures/vanilla.mdx'));
|
||||||
|
@ -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"
|
./node_modules/.bin/jscodeshift -t ./node_modules/@storybook/codemod/dist/transforms/convert-to-module-format.js . --ignore-pattern "node_modules|dist"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
export default {
|
export default {
|
||||||
title: 'Button',
|
title: 'Button',
|
||||||
@ -215,3 +217,39 @@ import { Meta, Story } from '@storybook/addon-docs/blocks';
|
|||||||
<Button label="Story 2" onClick={action('click')} />
|
<Button label="Story 2" onClick={action('click')} />
|
||||||
</Story>
|
</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',
|
||||||
|
};
|
||||||
|
```
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
"prepare": "node ../../scripts/prepare.js"
|
"prepare": "node ../../scripts/prepare.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@mdx-js/mdx": "^1.0.0",
|
||||||
"core-js": "^3.0.1",
|
"core-js": "^3.0.1",
|
||||||
"jscodeshift": "^0.6.3",
|
"jscodeshift": "^0.6.3",
|
||||||
"lodash": "^4.17.11",
|
"lodash": "^4.17.11",
|
||||||
|
@ -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>
|
@ -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',
|
||||||
|
};
|
@ -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>
|
@ -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',
|
||||||
|
};
|
@ -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>
|
@ -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',
|
||||||
|
};
|
@ -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>
|
@ -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',
|
||||||
|
};
|
@ -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>
|
@ -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,
|
||||||
|
},
|
||||||
|
};
|
@ -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}`);
|
||||||
|
});
|
146
lib/codemod/src/transforms/convert-mdx-to-module.js
Normal file
146
lib/codemod/src/transforms/convert-mdx-to-module.js
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user