Synvert

logo

write snippet code to rewrite your source code.

Step by Step Guideline

Here is a guideline that shows you how to write a snippet to rewrite JSX html tags.

The source code mentioned in this guideline can be found on github gist.

Snippet code: https://gist.github.com/flyerhzm/19136af12ef27335fe5e64093aee8f6f

Test code: https://gist.github.com/flyerhzm/90977c87b2d52f01953b71093bf9022d

Requirement

We want to convert

<div className="container-fluid">   ...   </div>

to

<Container fluid>  ... </Container>

Preparation

1. Clone synvert-snippets-javascript repo

$ git clone git@github.com:synvert-hq/synvert-snippets-javascript.git
$ cd synvert-snippets-javascript

Actually, you don’t need to clone the repo, but it makes test and debug the snippet code much easier.

2. Generate a template snippet

$ npx -p synvert synvert-javascript -g react/html-elements-to-react-bootstrap-components

If you’ve already installed the npm package synvert, you can simply run

$ synvert-javascript -g react/html-elements-to-react-bootstrap-components

It will generate a snippet file lib/react/html-elements-to-react-bootstrap-components.js and a test file test/react/html-elements-to-react-bootstrap-components.spec.js

3. Start test

$ npm install
$ npm run test_watch

It runs the test for the new snippet and you’ll see it’s passed now.

Write the snippet

1. Write test code

Change the test code in test/react/html-elements-to-react-bootstrap-components.spec.js

const input = `
  <div className="container-fluid">
    <div />
  </div>
`

const output = `
  <Container fluid>
    <div />
  </Container>
`

assertConvert({
  input,
  output,
  snippet,
  path: "code.jsx",
});

You just need to set the input and output code, then assert snippet will do the conversion.

Be sure to set path as a jsx file path so that it can parse the JSX tags.

Now you’ll see a test failure, next we should update the snippet code to pass the test.

2. Check the AST nodes

Before doing find and replace, I usually check the AST nodes first. Let’s change the code in lib/react/html-elements-to-react-bootstrap-components.js

const util = require('util');

new Synvert.Rewriter("react", "html-elements-to-react-bootstrap-components", () => {
  description("convert html elements to react bootstrap components");

  withinFiles(Synvert.ALL_FILES, function () {
    console.log(util.inspect(this.currentNode, { showHidden: false, depth: null, colors: true }));
  });
});

Synvert will parse the source code and save it to this.currentNode, we just need to print it, then we’ll see the AST nodes as follows

Node {
  type: 'Program',
  loc: SourceLocation {
    start: Position { line: 2, column: 4 },
    end: Position { line: 4, column: 10 },
  },
  body: [
    Node {
      type: 'ExpressionStatement',
      loc: SourceLocation {
        start: Position { line: 2, column: 4 },
        end: Position { line: 4, column: 10 },
      },
      expression: Node {
        type: 'JSXElement',
        loc: SourceLocation {
          start: Position { line: 2, column: 4 },
          end: Position { line: 4, column: 10 }
        },
        openingElement: Node {
          type: 'JSXOpeningElement',
          loc: SourceLocation {
            start: Position { line: 2, column: 4 },
            end: Position { line: 2, column: 37 }
          },
          attributes: [
            Node {
              type: 'JSXAttribute',
              loc: SourceLocation {
                start: Position { line: 2, column: 9 },
                end: Position { line: 2, column: 36 }
              },
              name: Node {
                type: 'JSXIdentifier',
                loc: SourceLocation {
                  start: Position { line: 2, column: 9 },
                  end: Position { line: 2, column: 18 }
                },
                name: 'className'
              },
              value: Node {
                type: 'Literal',
                loc: SourceLocation {
                  start: Position { line: 2, column: 19 },
                  end: Position { line: 2, column: 36 }
                },
                value: 'container-fluid',
                raw: '"container-fluid"'
              }
            }
          ],
          name: Node {
            type: 'JSXIdentifier',
            loc: SourceLocation {
              start: Position { line: 2, column: 5 },
              end: Position { line: 2, column: 8 }
            },
            name: 'div'
          },
          selfClosing: false
        },
        closingElement: Node {
          type: 'JSXClosingElement',
          loc: SourceLocation {
            start: Position { line: 4, column: 4 },
            end: Position { line: 4, column: 10 }
          },
          name: Node {
            type: 'JSXIdentifier',
            loc: SourceLocation {
              start: Position { line: 4, column: 6 },
              end: Position { line: 4, column: 9 }
            },
            name: 'div'
          }
        },
        children: [
          Node {
            type: 'JSXText',
            loc: SourceLocation {
              start: Position { line: 2, column: 37 },
              end: Position { line: 3, column: 6 }
            },
            value: '\n      ',
            raw: '\n      '
          },
          Node {
            type: 'JSXElement',
            loc: SourceLocation {
              start: Position { line: 3, column: 6 },
              end: Position { line: 3, column: 13 }
            },
            openingElement: Node {
              type: 'JSXOpeningElement',
              loc: SourceLocation {
                start: Position { line: 3, column: 6 },
                end: Position { line: 3, column: 13 }
              },
              attributes: [],
              name: Node {
                type: 'JSXIdentifier',
                loc: SourceLocation {
                  start: Position { line: 3, column: 7 },
                  end: Position { line: 3, column: 10 }
                },
                name: 'div'
              },
              selfClosing: true
            },
            closingElement: null,
            children: []
          },
          Node {
            type: 'JSXText',
            loc: SourceLocation {
              start: Position { line: 3, column: 13 },
              end: Position { line: 4, column: 4 }
            },
            value: '\n    ',
            raw: '\n    '
          }
        ]
      }
    }
  ],
  sourceType: 'module'
}

It omitted start, end, and source values

3. Write snippet code

We now get the whole AST nodes, we should find the div JSXElement, check if its className is container-fluid, then remove the className property, replace element name with Container and add fluid property

withinFiles(Synvert.ALL_FILES, function () {
  withNode({ type: "JSXElement", openingElement: { name: { name: "div" } } }, () => {
    let matched = false;
    gotoNode("openingElement", () => {
      withNode({ type: "JSXAttribute", name: { name: "className" }, value: { value: "container-fluid" } }, () => {
        matched = true;
        remove();
      });
    });
    if (matched) {
      replace('openingElement.name', { with: 'Container' });
      insert(' fluid', { to: 'openingElement.name' });
      replace('closingElement.name', { with: 'Container' });
    }
  });
});

I didn’t replace the whole element here, instead, I made 4 small conversions, the principle is to change the code as small part as possible so that you won’t change unnecessary code, like semicolon, single quote, and double code.

Now the test is passed.

4. A complex case

The snippet can handle the case that className is exact container-fluid, but what if className contains other value? Let’s add a new test case.

describe("complex", () => {
  const input = `
    <div className="container-fluid container-pagination" style={{ display: 'none' }}>
      <div />
    </div>
  `

  const output = `
    <Container fluid className="container-pagination" style={{ display: 'none' }}>
      <div />
    </Container>
  `

  assertConvert({
    input,
    output,
    snippet,
    path: "code.jsx",
  });
});

It’s failed again.

Then we can change the snippet code

withNode({ type: "JSXAttribute", name: { name: "className" }, value: { value: /container-fluid/ } }, function () {
  if (this.currentNode.value.value === "container-fluid") {
    remove();
  } else {
    const value = this.currentNode.value.value;
    replace('value', { with: `"${value.replace('container-fluid ', '').replace(' container-fluid', '')}"` });
  }
});

It checks if the value of className is exact container-fluid, it removes the whole property, otherwise, just remove the container-fluid value from the className.

Then it’s passed again.

5. import Container

We have changed the div element to Container component, the last thing we should do in the snippet is to import Container.

The test code should be

const input = `
  import React from 'react';

  <div className="container-fluid container-pagination" style={{ display: 'none' }}>
    <div />
  </div>
`

const output = `
  import React from 'react';
  import { Container } from 'react-bootstrap';

  <Container fluid className="container-pagination" style={{ display: 'none' }}>
    <div />
  </Container>
`

The snippet code should be

withinFiles(Synvert.ALL_FILES, function () {
  let needImport = false
  withNode({ type: "JSXElement", openingElement: { name: { name: "div" } } }, () => {
    ...
    if (matched) {
      needImport = true;
    }
  });
  if (needImport) {
    withNode({ type: "ImportDeclaration", source: { value: 'react' } }, function () {
      insert("\nimport { Container } from 'react-bootstrap';", { at: 'end' });
    });
  }
});

Run the snippet

We’ve already written the snippet and made tests passed, it’s time to use the snippet to change your repo’s code

$ npx -p synvert synvert-javascript --run react/html-elements-to-react-bootstrap-components --load ~/Sites/synvert-hq/synvert-snippets-javascript/lib/react/html-elements-to-react-bootstrap-components.js --enableEcmaFeaturesJsx --path ~/Sites/xinminlabs/awesomecode.io/app/javascript

If you have installed the synvert npm, you can also use it with a remote snippet.

$ synvert-javascript --run react/html-elements-to-react-bootstrap-components --load https://gist.githubusercontent.com/flyerhzm/19136af12ef27335fe5e64093aee8f6f/raw/78acebe47cc4e7130d62454b93ace6ea4832e276/html-elements-to-react-bootstrap-components.js --enableEcmaFeaturesJsx --path ~/Sites/xinminlabs/awesomecode.io/app/javascript