Skip to content

Latest commit

Β 

History

History
304 lines (215 loc) Β· 4.68 KB

slides.md

File metadata and controls

304 lines (215 loc) Β· 4.68 KB

title: "Codemods" controls: false progress: true numbers: true theme: tkers/cleaver-theme-sunset

Codemods

Refactoring JavaScript with JavaScript

background

πŸ™‹β€β™‚οΈ Tijn Kersjes

--

πŸ™‹β€β™‚οΈ Tijn Kersjes

  • Software engineer at Reaktor Amsterdam

  • Writing Node.js, React & TypeScript by day
  • Tinkering with compilers by night
  • github.com/tkers

--

What are codemods?

  • Also called program transformations
  • Metaprogramming
    • Automatise programming / refactoring
  • Can be applied to your entire code base
  • Modifies program using its AST

--

Basic code refactoring

Function name is updated:

export {
-  foo
+  bar
}

Find and replace πŸ’ͺ

import {
-  foo
+  bar
} from './myLib.js'

// ...

- foo()
+ bar()

--

However...

No access to the semantics of your code:

import { foo as fooA } from './myLib.js'
import { foo as fooB } from './otherLib.js'

obj.foo = 42

sendMessage('foo')

--

Changing code structure

Binding manually:

class MyComponent extends React.Component {
  constructor() {
    this.handleClick = this.handleClick.bind(this)
  }

  handleClick() {
    this.setState({ open: true })
  }
}

Using arrow function as class property:

class MyComponent extends React.Component {
  handleClick = () => this.setState({ open: true })
}

Modify the program instead of text!

--

Abstract Syntax Tree 🌲

class MyComponent extends React.Component {
  constructor() {
    this.handleClick = this.handleClick.bind(this)
  }

  handleClick() {
    this.setState({ open: true })
  }
}
  • ClassDeclaration MyComponent
    • MethodDefinition constructor
      • AssignmentExpression =
        • MemberExpression this.handleClick
        • CallExpression bind()
    • MethodDefinition handleClick
      • CallExpression setState()

--

AST Explorer

astexplorer.net

--

Now what? πŸ€”

Tools using the AST:

  • ESLint reads (and reports issues)
  • Prettier reprints (ignoring whitespaces)
  • Babel transforms (ES6 to ES5)
  • ...

--

JSCodeshift

  • AST transformation tool
  • Wrapper around Recast
    • Preserves formatting
  • Update React APIs

--

Basic transformation

export default function transform(file, api) {
  const j = api.jscodeshift
  const root = j(file.source)

  // ...transform root

  return root.toSource()
}

--

Our codebase

  • 100 KLOC
  • 50 developers
  • 20 merges/day
  • A lot of time pressure πŸ’©

--

State assignment in constructor:

class MyComponent extends React.Component {
  constructor() {
    this.state = {
      foo: 'hello',
      bar: 42
    }
  }
}

State as class property:

class MyComponent extends React.Component {
  state = {
    foo: 'hello',
    bar: 42
  }
}

--

State-to-class-property transform

export default function transform(file, api) {
  const j = api.jscodeshift
  const root = j(file.source)

  root
    .find(j.ClassDeclaration)
    .find(j.MethodDefinition, { kind: 'constructor' })
    .find(j.AssignmentExpression, {
      left: {
        object: j.ThisExpression,
        property: { name: 'state' }
      }
    })
    .forEach(stateAssignment => {
      const initialState = stateAssignment.node.right
      const asProperty = j.classProperty(j.identifier('state'), initialState, null, false)

      j(stateAssignment)
        .closest(j.ClassBody)
        .get('body')
        .insertAt(0, asProperty)

      j(stateAssignment).remove()
    })

  return root.toSource()
}

--

Resulting code πŸ™Œ

Almost what we wanted:

class MyComponent extends React.Component {
  state = {
    foo: 'hello',
    bar: 42
  }
  constructor() {}
}

--

More transformations!

--

Apply it to the repo 🀞

  • Manual git diff check
  • Automated tests

commit info

--

Some important painpoints

  • Documentation is scarce
  • JSCodeshift API a bit confusing
  • JavaScript is messy
    • A lot of edge cases
    • Focus on the 99%

--

Super fast refactoring πŸš€

  • Wow-factor
  • Updating function interfaces
  • Postpone design decisions

-- dark

That's it for today πŸ‘Œ

background