How we build and operate the Keboola data platform
Vladimír Kriška 5 min read

How we converted CoffeeScript files with about 220 React components to JavaScript

On June 11, 2018 I created a new issue Rewrite from CoffeeScript to JS/JSX to get rid of CoffeeScript in our biggest repository for Keboola Connection UI .

How we converted CoffeeScript files with about 220 React components to JavaScript
CoffeeScript files in kbc-ui repository by date

Why?

JavaScript and its ecosystem already provide features and tools we need. These features (arrow functions, object spread operator, destructuring, etc.) are stable enough and there’s no need to write code in another language and convert it to JavaScript. Maybe in TypeScript, but that’s another story.


History

Before I created the issue, we were already converting CoffeeScript to JavaScript. We had a non-written rule in our team that when you need to touch .coffee file, you should turn it to .js(x) first. We didn’t standardize this process in any way, so someone converted it using the CLI, someone using the online tool js2.coffee.

Later we tuned that process a bit. We started writing tests/snapshots to be sure we are not breaking things. Check the article The worry-free way to convert CoffeeScript to JSX to find out more.

This process wasn’t so bad. The biggest drawback was that we needed to rewrite React components almost from scratch after every conversion.

Our sample CoffeeScript looked like this:

React = require 'react'
{ div, p } = React.DOM
module.exports = React.createClass
  render: ->
      div null,
        p className: 'text-center',
          'Hello World!'

And converted by the online tool js2coffee:

var React, div, p, ref;
React = require('react');
ref = React.DOM, div = ref.div, p = ref.p;
module.exports = React.createClass({
  render: function() {
    return div(null, p({
      className: 'text-center'
    }, 'Hello World!'));
  }
});

As you can see, the render function was very simple, but after the conversion we weren’t able to easily modify it. Every element was converted to a function.


Decaffeinate

In August 2018 I bumped into an article about a similar process: Converting a large React Codebase from CoffeeScript to ES6. It was a huge time saver for us. Especially for introduction to Decaffeinate — a tool for conversion CoffeeScript to JavaScript.

Decaffeeinate is very powerful, and after few tests, I found it can work for us.

decaffeinate --loose-includes --loose-default-params --loose-for-expressions --loose-for-of --use-js-modules sample.coffee

Produces this output:

import React from 'react';
const { div, p } = React.DOM;
export default React.createClass({
  render() {
      return div(null,
        p({className: 'text-center'},
          'Hello World!')
      );
    }
});

But the problem remains the same. Even if the code looks fine, has nice ES6 imports (no require), nice default ES6 export, and no function keyword, it still has functions instead of JSX elements.


Jscodeshift

I knew there’s a tool for automatically converting a file from one JavaScript syntax to another. It’s called Jscodeshift. I saw it at some conference.

Secondly, I also knew that Facebook provides scripts (called transform scripts or simply transforms) to convert React components from one syntax to another, so transitions to newer versions of React should be smoother.

I noticed that in the mentioned article about converting to ES6, they were using jscodeshift too. So I started exploring various transforms in the react-codemod repository. Unfortunately, with no luck. I had to find another way.

I found the React-DOM-to-react-dom-factories.js transform script. It should be able to convert React.DOM.div() calls to React.createElement('div') calls. But we weren’t there yet.

Custom transform script

Then I decided to write a transformation script. To replace div() calls by React.DOM.div() calls and to remove variables assigned using React.DOM object destructuring (const { div, p } = React.DOM;).

I spent few hours playing with AST explorer — exploring AST generated from a JavaScript file from Decaffeinate output and programming my own transform script.

The transform script does this:

  • It finds functions (if path.name === 'callee', then it’s a function) with the DOM factory name isDomFactory(path.node.name) and adds the prefix React.DOM..
  • Then it finds the parent for React.DOM VariableDeclarator and removes the whole line.

Which finally modifies our component to:

import React from 'react';
export default React.createClass({
  render() {
      return React.DOM.div(null,
        React.DOM.p({className: 'text-center'},
          'Hello World!')
      );
    }
});

See dom-factories-calls-to-calls-with-react-dom-prefix.js to check the full script.


Putting things together

After a successful conversion using a custom transform script, we can run the React-DOM-to-react-dom-factories.js:

import React from 'react';
export default React.createClass({
  render() {
      return React.createElement(
        'div',
        null,
        React.createElement('p', {className: 'text-center'}, 'Hello World!')
      );
    }
});

And then another transform script create-element-to-jsx.js:

import React from 'react';
export default React.createClass({
  render() {
      return (
        <div>
          <p className="text-center">
            Hello World!
          </p>
        </div>
      );
    }
});

Finally, we have a nice ES6 component with clean JSX syntax. Great.

All these steps can be put together into a simple shell script, which should be run in a directory with the .coffee files you want to convert.

decaffeinate --loose-includes --loose-default-params --loose-for-expressions --loose-for-of --use-js-modules *.coffee

jscodeshift -t /code/codemod/transforms/dom-factories-calls-to-calls-with-react-dom-prefix.js *.js

jscodeshift -t /code/react-codemod/transforms/React-DOM-to-react-dom-factories.js *.js

jscodeshift -t /code/react-codemod/transforms/create-element-to-jsx.js *.js

How did it work for us?

We tried to convert all files at once, but after the conversion, there were about 3,300 Eslint errors. Ignoring them was not an option, so continuous transformation worked better.

The key was to create small pull requests with a conversion of a few modules to move fast without breaking things.

We already had experience with this. During migration from React 13 to React 14, we did it the same way.

To replace CoffeeScript, we created 96 pull requests and tracked progress automatically every day. A simple script checked a number of remaining files and created a Markdown file with stats. After that Travis CI built it using CRON job every day. Then it was published as a static website to Github Pages.

From the issue creation to the end, it took about five months and two weeks. But as you can see, a more significant decrease occurred already at the beginning of September. That’s when our colleague Jakub Kotek began working on the issue and pushed it forward really fast.

CoffeeScript files in kbc-ui repository by date

However, we weren’t working on it full-time. No one would have approved it. Ever. Because the immediate business value of the conversion itself is zero. No UI polishing, no speed improvements, nothing. Z-E-R-O. But it will allow us to do things a bit faster in the future.

Technically, there were small issues we found during conversion:

  • Many components needed a manual replacement for React.createFactory (we decided not to write another transform script, because the manual update was really fast).
  • Replacement of __guard__ functions created by Decaffeinate was a bit complicated sometimes.
  • We had to be careful with imports of default exports from new JS/JSX files. The imports had to be replaced in the remaining .coffee files by adding the .default suffix.

November 25th, 2018 was the last day we saw CoffeeScript in our main UI repository.

If you liked this article please share it.

Comments ()

Read next

MySQL + SSL + Doctrine

MySQL + SSL + Doctrine

Enabling and enforcing SSL connection on MySQL is easy: Just generate the certificates and configure the server to require secure…
Ondřej Popelka 8 min read