Skip to content

Instantly share code, notes, and snippets.

@DogPawHat
Created January 27, 2020 14:08
Show Gist options
  • Save DogPawHat/651b7fca73fed0baf21078320eee06c4 to your computer and use it in GitHub Desktop.
Save DogPawHat/651b7fca73fed0baf21078320eee06c4 to your computer and use it in GitHub Desktop.
Baby's first code mod

Baby's first codemod

What madness is this?

So I was working on a project recently to update the documentation around Storybook, which involves updating to Storybook 5.3

A neat little feature her is the ablity to write stories and documentaiton in MDX, a glorius love child of JSX and Markdown that enables you to put React componetns into a Markdown file and have it come out the other end.

<Meta title="Examples" decorators={[withKnobs]}/>

# This is a story

<Story name="example story">
  <ExampleReactComponent>
    {text('HelloWorld', 'Hello brave new storybook world!')}
  </ExampleReactComponent>
</Story>

Now, it's still in it's toddler years (the extentions for VSCode and prettier still need work), but it already works pretty well for Storybook.

So we had a bunch of old stories that I wanted to move to the new format that looked like this

const stories = storiesOf('Examples', module);

stories.addDecorator(withKnobs);

stories.add('example story', () => (
  <Story name="example story">
    <ExampleReactComponent>
      {text('HelloWorld', 'Hello brave new storybook world!')}
    </ExampleReactComponent>
  </Story>
));

Now, there are two codes you can get from storybook (https://www.npmjs.com/package/@storybook/codemod) for this example:

  • One to go from the storiesOf format to CSF (stories as default and named exports)
export default {
  title="Examples"
  component={ExampleReactComponent}
  decorators={[withKnobs]}
}

export const exampleStory = () => (
  <Story name="example story">
    <ExampleReactComponent>
      {text('HelloWorld', 'Hello brave new storybook world!')}
    </ExampleReactComponent>
  </Story>
);
  • Once to change the CSF to MDX

So, if we have a bunch of storiesOf stories, we just run it though both code mods and were done, right?

Well, no.

See, it turns out the storiesof-to-csf is expecting a chainable style like this:

storiesOf('Examples', module)
  .addDecorator(withKnobs)
  .add(
    'example story', () => (
      <Story name="example story">
        <ExampleReactComponent>
          {text('HelloWorld', 'Hello brave new storybook world!')}
        </ExampleReactComponent>
      </Story>
  ));

And the style we had were we ran everything off a stories variable wasn't what the code mod was expecting.

So, we need a third codemod now, to collapse our const stories style into a chainable style

Now, it's possible someone somewhere has done this better then me, but it's niche enough that it's not gaurenteed, and since I have done some tree traversing in the past in the past with things like XML and such, I think the AST used in jscodeshift wouldn't be that hard.

Now I end up mostly writing and testing this script in https://astexplorer.net/ since it gave me an way to figure out when the thing was working, skimming this tutorial https://www.toptal.com/javascript/write-code-to-rewrite-your-code and the documentation as needed. Flow of the program is described in some code comments, it should be easy enought to follow

It took me about 4 hours to do and figure out, and it's probaly not the best way to do this, but hey, I think it's neat, don't @ me.

// Press ctrl+space for code completion
export default function transformer(file, api) {
const j = api.jscodeshift;
// set up root
const root = j(file.source);
// some bit and bobs for later
const storyMatcher = {
name: "stories"
}
const storyId = root.find(j.VariableDeclarator, {
id: {name: "stories"}
});
const storyExpressions = root.find(j.ExpressionStatement, {
expression: {
type: "CallExpression",
callee: {
type: "MemberExpression",
object: storyMatcher
}}
});
const storyVar = root.find(
j.VariableDeclaration,
{
declarations: [
{
type: "VariableDeclarator",
id: storyMatcher
}
]
}
)
/** `const stories = storiesOf('yadaYada') -> storiesOf('yadaYada') */
const newExpression = storyVar
.replaceWith(nodePath => {
const { node } = nodePath;
const init = storyId.get(0).node.init;
const obj = j.expressionStatement(init);
return obj;
});
/**
* For all statements of format `stories.doSomething(something)`,
* having already changed the stories variable declaration,
* collapese all the statments into the same chainable expression
*
* e.g `stories.doSomething(something) -> storiesOf('yadaYada).doSomething(something)
* followed by `stories.doSomethingElse(somethingElse) storiesOf('yadaYada).doSomething(something).stories.doSomethingElse(somethingElse)
* */
storyExpressions.forEach(SENP => {
const newCallee = SENP.node.expression
newExpression.replaceWith(NENP => {
const NE = NENP.node
const newMember = j.memberExpression(
NE.expression,
SENP.node.expression.callee.property,
false
);
const obj = j.expressionStatement(
j.callExpression(newMember,
newCallee.arguments
)
);
return obj;
})
} ).remove();
return newExpression
.toSource();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment