JS Refactoring toolkit

Bartosz Szewczyk

>5 @ Codete

2021

Me after half-marathon

Me after half marathon

Game I made in my free time

Overview

  1. Problem explanation
  2. Regular expressions
  3. Comby.dev
  4. Jscodeshift (codemods)
  5. Summary

Problem:

Rewriting code can be hard

Example

Styled system v4

Example

Styled system v5

Breaking change

😵

Complexity

Scenario #1

Scenario #2

Scenario #3

Scenario #4

Scenario #5

Scenario #6

Scenario #7

Bonus cases

Not covered

Scenario #8

Scenario #9

Regular expressions

String that specifies a search pattern

How to use it?

🤔

Useful tool #1

Your code editor/IDE

Regexp in Vs-code

Useful tool #2

Perl interpreter 🤓

MATCH="\
(import {[\s\S]*)\
(themeGet,?)\
([\s\S]*} from 'styled-system';)\
"

REPLACE="\
\1\3
import { themeGet } from '\@styled-system/theme-get'\;
"

perl -i -0pe "s|$MATCH|$REPLACE|gm" src/**/*.js
          

Useful tool #3

Just use any scripting language

const fs = require('fs')
const glob = require('glob')
const regexp = /.../g
const replace = '...'

glob('src/**/*.{ts,tsx}', (err, files) => {
  files.forEach(filename => {
    const contents =
      fs.readFileSync(filename, { encoding: 'utf-8' })

    if (contents.indexOf('themeGet') !== -1) {
      fs.writeFileSync(
        filename,
        contents.replace(regexp, replace)
      )
    }
  })
})

Pros:

  1. Widely supported
  2. Ton of tutorials
  3. Lot's of assist tools

Cons:

  1. Low maintainability
  2. Low readability
  3. Doesn't scale
  4. Need 2 replaces

Scenario #1 (reminder)

🧙‍♀️ regexp magic 🧙

Comby.dev

comby-logo

Comby is a tool for searching and rewriting code

Highlights

  1. Capturing groups (regexp) -> Holes
  2. Holes can be named
  3. Holes can (optionally) accept regexp
  4. No need to escape any chars (really!)

What is comby.dev syntax

  • named hole :[var]
  • named hole with regex :[var~regex]
  • nameless hole with regex:[~regex]
Imagine that we want to replace
Into ♻️

How we can solve it with regex?

Time for Comby.dev

Some useful comby.dev features

Let's use Comby.dev for themeGet import

CLI

COMBY_M="$(cat <<"MATCH"
import {
:[before]themeGet:[~,?]:[after]
} from 'styled-system'
MATCH
)"

COMBY_R="$(cat <<"REWRITE"
import {:[before]:[after]} from 'styled-system'
import { themeGet } from '@styled-system/theme-get'
REWRITE
)"

comby "$COMBY_M" "$COMBY_R" .js -i -d src

Pros:

  1. Super easy
  2. Dedicated for code
  3. Web playground for testing
  4. Dedicated gitter channel
  5. Ready-to-use cli command

Cons:

  1. Low maintainability true
  2. Low readability false
  3. Doesn't scale true
  4. Need 2 replaces true

JsCodeShift

aka Codemods

What are codemods

Code that rewrites code

Code that operate on code

Code that operate on code AST

Video of AST explorer

light mode warning ⚠️

Light mode alert

JsCodeShift API showcase


export const transform = (fileInfo, api) => {
  const { jscodeshift: j } = api;

  return j(fileInfo.source)
    .find(/* styled-system import with themeGet */)
    .replaceWith(/* import without themeGet */)
    .insertAfter(/* @styled-system/theme-get import */)
    .toSource();
};
          

Time for for codmod writing

Live coding gif

const transform: Transform = (fileInfo, api) => {
  const { jscodeshift: j } = api;

  const isThemeGet = (specifier) =>
    specifier.type === 'ImportSpecifier' &&
    specifier.imported.name === 'themeGet';

  return j(fileInfo.source)
    .find(j.ImportDeclaration, {
      source: { value: 'styled-system' },
      specifiers: (specifiers) =>
        specifiers?.some(isThemeGet) ?? false,
    })
    .replaceWith(({ node }) => {
      const specifiersWithoutThemeGet =
        node.specifiers?.filter(
          (specifier) =>
            !isThemeGet(specifier)
        ) ?? [];

      return specifiersWithoutThemeGet.length > 0
        ? j.importDeclaration(
            specifiersWithoutThemeGet,
            node.source,
            node.importKind
          )
        : null;
    })
    .insertAfter(
      j.template
        .statement`
          import {
            theme-get
          } from '@styled-system/theme-get'
        `
    )
    .toSource();
};
          

Usage

cli


jscodeshift -t theme-get.ts src/**/*.js
          

Bonus

Code can be easily unit tested


defineInlineTest(
  transform,
  {},
  `
import { a, themeGet, b } from 'styled-system';
  `,
  `
import { a, b } from 'styled-system';
import { themeGet } from '@styled-system/theme-get';
  `
)
          

Pros:

  1. Extremely powerful
  2. Typescript support
  3. Easy setup for unit tests

Cons:

  1. Low maintainability false
  2. Low readability false
  3. Doesn't scale false
  4. Need 2 replaces false
  5. Poor documentation
  6. Entry barrier (not that scary)

Summary 📋

Is there the best option?

Cat with face that says no

No

What to keep in mind (a recap 📝)

  1. Regular expressions are useful to know
  2. Comby is super easy for simple code rewrite
  3. Codemods takes time for setup but outmatches others in complicated cases

Thank you

my face

Bartosz Szewczyk

👨🏼‍💻 Tech Lead @ Codete

Thank you gif

Useful links