1 of 233

Babel:�A refactoring tool

@NicoloRibaudo

2 of 233

2

NICOLÒ RIBAUDO

Babel team

@nicolo-ribaudo

@NicoloRibaudo

nicolo.ribaudo@gmail.com

@NicoloRibaudo

3 of 233

3

@babel

@babeljs

https://babeljs.io

https://opencollective.com/babel

@NicoloRibaudo

4 of 233

What is Babel?

4

@NicoloRibaudo

5 of 233

What is Babel?

Babel is a JavaScript compiler

5

@NicoloRibaudo

6 of 233

It’s a JavaScript to JavaScript compiler

player.level ??= 70_000;

(_player$level = player.level) != null

? _player$level

: player.level = 70000;

6

@NicoloRibaudo

7 of 233

7

@NicoloRibaudo

8 of 233

8

@babel/how-to

@NicoloRibaudo

9 of 233

What is Babel?

9

9

@NicoloRibaudo

10 of 233

What is Babel?

Babel is a customizable JavaScript compiler

10

@NicoloRibaudo

11 of 233

So… what can I do?

11

@NicoloRibaudo

12 of 233

So… what can I do?

  • Compile modern ECMAScript syntax to older (and more supported) syntax
  • Compile TypeScript, Flow and JSX to plain JavaScript

12

@NicoloRibaudo

13 of 233

So… what can I do?

  • Compile modern ECMAScript syntax to older (and more supported) syntax
  • Compile TypeScript, Flow and JSX to plain JavaScript

  • Minify your code

13

@NicoloRibaudo

14 of 233

So… what can I do?

  • Compile modern ECMAScript syntax to older (and more supported) syntax
  • Compile TypeScript, Flow and JSX to plain JavaScript

  • Minify your code

  • Statically evaluate parts of your program

14

@NicoloRibaudo

15 of 233

So… what can I do?

  • Compile modern ECMAScript syntax to older (and more supported) syntax
  • Compile TypeScript, Flow and JSX to plain JavaScript

  • Minify your code

  • Statically evaluate parts of your program

  • Perform static analysis on your code

15

@NicoloRibaudo

16 of 233

So… what can I do?

  • Compile modern ECMAScript syntax to older (and more supported) syntax
  • Compile TypeScript, Flow and JSX to plain JavaScript

  • Minify your code

  • Statically evaluate parts of your program

  • Perform static analysis on your code

  • Run one-time transforms and automate refactorings

16

@NicoloRibaudo

17 of 233

So… what can I do?

  • Run one-time transforms and automate refactorings

17

@NicoloRibaudo

18 of 233

Codemods

One-time transforms and automate refactorings

18

@NicoloRibaudo

19 of 233

Codemods

19

@NicoloRibaudo

20 of 233

Codemods

Refactors… happen.

20

@NicoloRibaudo

21 of 233

Codemods

Refactors… happen.

21

Sometimes ...

And they take ...

… they are self-contained, without impacting other parts or your program

… a few hours

@NicoloRibaudo

22 of 233

Codemods

Refactors… happen.

22

Sometimes ...

And they take ...

… they are self-contained, without impacting other parts or your program

… a few hours

… they affects your whole codebase

… a week

@NicoloRibaudo

23 of 233

Codemods

Refactors… happen.

23

Sometimes ...

And they take ...

… they are self-contained, without impacting other parts or your program

… a few hours

… they affects your whole codebase

… a week

… you need to introduce changes accross applications maintained by different teams

… many weeks, maybe

months months

@NicoloRibaudo

24 of 233

Codemods

Refactors… happen.

24

Sometimes ...

And they take ...

… they are self-contained, without impacting other parts or your program

… a few hours

… they affects your whole codebase

… a week

… you need to introduce changes accross applications maintained by different teams

… many weeks, maybe

months months

… you maintain a popular library that is used by many users outside of your company

… ?

@NicoloRibaudo

25 of 233

What can you use codemods for?

25

@NicoloRibaudo

26 of 233

What can you use codemods for?

What did we use them for in Babel?

26

@NicoloRibaudo

27 of 233

What can you use codemods for?

What did we use them for in Babel?

  • Migrate our tests to Jest 🔗

27

@NicoloRibaudo

28 of 233

What can you use codemods for?

What did we use them for in Babel?

  • Migrate our tests to Jest 🔗

  • Remove unused catch bindings 🔗

28

@NicoloRibaudo

29 of 233

What can you use codemods for?

What did we use them for in Babel?

  • Migrate our tests to Jest 🔗

  • Remove unused catch bindings 🔗

  • Remove the resolve dependency 🔗

29

@NicoloRibaudo

30 of 233

What can you use codemods for?

What did we use them for in Babel?

  • Migrate our tests to Jest 🔗

  • Remove unused catch bindings 🔗

  • Remove the resolve dependency 🔗

  • Migrate from Flow to TypeScript 🔗

30

@NicoloRibaudo

31 of 233

What can you use codemods for?

What other companies use them for?

31

@NicoloRibaudo

32 of 233

What can you use codemods for?

What other companies use them for?

  • Facebook publishes codemods to migrate away from legacy React versions 🔗

32

@NicoloRibaudo

33 of 233

What can you use codemods for?

What other companies use them for?

  • Facebook publishes codemods to migrate away from legacy React versions 🔗

  • Gatsby provides codemods to migrate to new versions 🔗 🔗

33

@NicoloRibaudo

34 of 233

What can you use codemods for?

What other companies use them for?

  • Facebook publishes codemods to migrate away from legacy React versions 🔗

  • Gatsby provides codemods to migrate to new versions 🔗 🔗

  • Next.js provides codemods to upgrade when a feature is deprecated 🔗

34

@NicoloRibaudo

35 of 233

Compilers vs Codemods

35

@NicoloRibaudo

36 of 233

Compilers vs Codemods

36

@NicoloRibaudo

37 of 233

Compilers vs Codemods

The usual Babel transforms are based on strictly defined semantics

37

@NicoloRibaudo

38 of 233

Compilers vs Codemods

The usual Babel transforms are based on strictly defined semantics

person?.address.city?.size

person == null

? void 0

: (_tmp = person.address.city) == null

? void 0

: _tmp.size

38

@NicoloRibaudo

39 of 233

Compilers vs Codemods

Codemods are based on assumptions about your coding style

The usual Babel transforms are based on strictly defined semantics

person?.address.city?.size

person == null

? void 0

: (_tmp = person.address.city) == null

? void 0

: _tmp.size

39

@NicoloRibaudo

40 of 233

Compilers vs Codemods

Codemods are based on assumptions about your coding style

person

&& person.address.city

&& person.address.city.size

person?.address.city?.size

The usual Babel transforms are based on strictly defined semantics

person?.address.city?.size

person == null

? void 0

: (_tmp = person.address.city) == null

? void 0

: _tmp.size

40

@NicoloRibaudo

41 of 233

Compilers vs Codemods

Codemods are based on assumptions about your coding style

person

&& person.address.city

&& person.address.city.size

person?.address.city?.size

false?.address.city?.size

The usual Babel transforms are based on strictly defined semantics

person?.address.city?.size

person == null

? void 0

: (_tmp = person.address.city) == null

? void 0

: _tmp.size

41

@NicoloRibaudo

42 of 233

Compilers vs Codemods

Codemods are based on assumptions about your coding style

The usual Babel transforms are based on strictly defined semantics

42

@NicoloRibaudo

43 of 233

Compilers vs Codemods

Codemods are based on assumptions about your coding style

The usual Babel transforms are based on strictly defined semantics

They need to be precise and infallible.

Developers should be able to trust the compiler output without checking it every time.

43

@NicoloRibaudo

44 of 233

Compilers vs Codemods

Codemods are based on assumptions about your coding style

They need to work in most cases, but it's not necessary that they work always.

The usual Babel transforms are based on strictly defined semantics

They need to be precise and infallible.

Developers should be able to trust the compiler output without checking it every time.

44

@NicoloRibaudo

45 of 233

Compilers vs Codemods

Codemods are based on assumptions about your coding style

They need to work in most cases, but it's not necessary that they work always.

They rely on human intervention.

The usual Babel transforms are based on strictly defined semantics

They need to be precise and infallible.

Developers should be able to trust the compiler output without checking it every time.

45

@NicoloRibaudo

46 of 233

Human-assisted automated transforms

46

Why don't we just create the perfect codemod?

@NicoloRibaudo

47 of 233

Human-assisted automated transforms

47

@NicoloRibaudo

48 of 233

Human-assisted automated transforms

const fs = require("fs");

const { join } = require("path");

48

@NicoloRibaudo

49 of 233

Human-assisted automated transforms

const fs = require("fs");

const { join } = require("path");

import fs from "fs";

import { join } from "path";

49

@NicoloRibaudo

50 of 233

Human-assisted automated transforms

const fs = require("fs");

const { join } = require("path");

exports.readLocal = (file) => {

return fs.readFileSync(

join(__dirname, file)

);

};

import fs from "fs";

import { join } from "path";

50

@NicoloRibaudo

51 of 233

Human-assisted automated transforms

const fs = require("fs");

const { join } = require("path");

exports.readLocal = (file) => {

return fs.readFileSync(

join(__dirname, file)

);

};

import fs from "fs";

import { join } from "path";

export const readLocal = (file) => {

return fs.readFileSync(

join(__dirname, file)

);

}

51

@NicoloRibaudo

52 of 233

Human-assisted automated transforms

const fs = require("fs");

const { join } = require("path");

exports.readLocal = (file) => {

return fs.readFileSync(

join(__dirname, file)

);

};

// "writeLocal"

exports[`${fs.write.name}Local`] =

(file, contents) => {

return fs.writeFileSync(

join(__dirname, file), contents

);

};

import fs from "fs";

import { join } from "path";

export const readLocal = (file) => {

return fs.readFileSync(

join(__dirname, file)

);

}

52

@NicoloRibaudo

53 of 233

Human-assisted automated transforms

const fs = require("fs");

const { join } = require("path");

exports.readLocal = (file) => {

return fs.readFileSync(

join(__dirname, file)

);

};

// "writeLocal"

exports[`${fs.write.name}Local`] =

(file, contents) => {

return fs.writeFileSync(

join(__dirname, file), contents

);

};

import fs from "fs";

import { join } from "path";

export const readLocal = (file) => {

return fs.readFileSync(

join(__dirname, file)

);

}

// "writeLocal"

export const UNKNOWN =

(file, contents) => {

return fs.writeFileSync(

join(__dirname, file), contents

);

};

53

@NicoloRibaudo

54 of 233

Human-assisted automated transforms

54

@NicoloRibaudo

55 of 233

Alternatives

55

@NicoloRibaudo

56 of 233

Alternatives

Regular Expressions

Good for simple, self-contained refactors

56

@NicoloRibaudo

57 of 233

Alternatives

JSCodeshift

The most popular tool to build JavaScript codemods

57

@NicoloRibaudo

58 of 233

Alternatives

JSCodeshift

The most popular tool to build JavaScript codemods

  • Provides a jQuery-like API to navigate the AST

58

@NicoloRibaudo

59 of 233

Alternatives

JSCodeshift

The most popular tool to build JavaScript codemods

  • Provides a jQuery-like API to navigate the AST

  • Includes different tools (@babel/parser for parsing, recast for printing)

59

@NicoloRibaudo

60 of 233

Alternatives

JSCodeshift

The most popular tool to build JavaScript codemods

  • Provides a jQuery-like API to navigate the AST

  • Includes different tools (@babel/parser for parsing, recast for printing)

  • It can be used with any AST transform tool you like (Babel included!)

60

@NicoloRibaudo

61 of 233

Alternatives

JSCodeshift

The most popular tool to build JavaScript codemods

So… why Babel?

61

@NicoloRibaudo

62 of 233

Alternatives

JSCodeshift

The most popular tool to build JavaScript codemods

So… why Babel?

  • It provides a massive set of built-in utilities to analyze the AST and the Scope

62

@NicoloRibaudo

63 of 233

Alternatives

JSCodeshift

The most popular tool to build JavaScript codemods

So… why Babel?

  • It provides a massive set of built-in utilities to analyze the AST and the Scope

  • You can re-use the same knowledge to modify your build process with custom plugins

63

@NicoloRibaudo

64 of 233

A practical example

64

@NicoloRibaudo

65 of 233

A practical example

Transforming React class components to functions with hooks

65

@NicoloRibaudo

66 of 233

66

@NicoloRibaudo

67 of 233

67

React class components are still supported and won't go away soon.

@NicoloRibaudo

68 of 233

68

React class components are still supported and won't go away soon.

This codemod is for illustrative purposes, you don't actually need to do it!

@NicoloRibaudo

69 of 233

class Counter extends React.Component {

state = { name: "Unknown", count: 0 };

incrementCount = () => {

this.setState((state) => ({ count: state.count - 1 }));

};

decrementCount = () => {

this.setState((state) => ({ count: state.count - 1 }));

};

updateName = (e) => {

this.setState({ name: e.target.value });

};

render() {

return (

<div>

<label>

${this.props.label}:

<input type="text" defaultValue={this.state.name}

onChange={this.updateName} />

</label>

<button onClick={this.decrementCount}>-1</button>

<span>{this.state.count}</span>

<button onClick={this.incrementCount}>+1</button>

</div>

);

}

}

const Counter = (props) => {

const [name, setName] = useState("Unknown");

const [count, setCount] = useState(0);

const incrementCount = () => {

setCount((count) => count - 1);

};

const decrementCount = () => {

setCount((count) => count - 1);

};

const updateName = (e) => {

setName(e.target.value);

};

return (

<div>

<label>

${props.label}:

<input type="text" defaultValue={name}

onChange={updateName} />

</label>

<button onClick={decrementCount}>-1</button>

<span>{count}</span>

<button onClick={incrementCount}>+1</button>

</div>

);

};

69

@NicoloRibaudo

70 of 233

70

@NicoloRibaudo

71 of 233

AST Explorer

A plugin development playground

71

@NicoloRibaudo

72 of 233

AST Explorer: a plugin playground

72

@NicoloRibaudo

@NicoloRibaudo

73 of 233

AST Explorer: a plugin playground

73

@NicoloRibaudo

Input code

Input AST

Babel plugin

Output code

@NicoloRibaudo

74 of 233

74

@NicoloRibaudo

75 of 233

Steps

75

@NicoloRibaudo

76 of 233

Steps

  1. Detecting React class components

76

Complexity

@NicoloRibaudo

77 of 233

Steps

  • Detecting React class components
  • Transforming the render() method

77

Complexity

@NicoloRibaudo

78 of 233

Steps

  • Detecting React class components
  • Transforming the render() method
  • Adding support for props

78

Complexity

@NicoloRibaudo

79 of 233

Steps

  • Detecting React class components
  • Transforming the render() method
  • Adding support for props
  • Adding support for state

79

Complexity

@NicoloRibaudo

80 of 233

Steps

  • Detecting React class components
  • Transforming the render() method
  • Adding support for props
  • Adding support for state

    • Not adding support for complex state (with useReducer())

80

Complexity

@NicoloRibaudo

81 of 233

Steps

  • Detecting React class components
  • Transforming the render() method
  • Adding support for props
  • Adding support for state

    • Not adding support for complex state (with useReducer())
    • Analytics: Should we support defaultProps?

81

Complexity

@NicoloRibaudo

82 of 233

Steps

  • Detecting React class components
  • Transforming the render() method
  • Adding support for props
  • Adding support for state

    • Not adding support for complex state (with useReducer())
    • Analytics: Should we support defaultProps?

  • Adding support for class fields

82

Complexity

@NicoloRibaudo

83 of 233

Steps

  • Detecting React class components
  • Transforming the render() method
  • Adding support for props
  • Adding support for state

    • Not adding support for complex state (with useReducer())
    • Analytics: Should we support defaultProps?

  • Adding support for class fields
  • Injecting imports to hooks

83

Complexity

@NicoloRibaudo

84 of 233

The anatomy of a Babel plugin

function codemod({ types: t, template }) {

// ...

return {

visitor: {

// ...

}

};

}

84

@NicoloRibaudo

85 of 233

The anatomy of a Babel plugin

function codemod({ types: t, template }) {

// ...

return {

visitor: {

// ...

}

};

}

85

Get different utilities from Babel, ...

@NicoloRibaudo

86 of 233

The anatomy of a Babel plugin

function codemod({ types: t, template }) {

// ...

return {

visitor: {

// ...

}

};

}

86

Get different utilities from Babel, ...

… define helper functions to analyze and transform ...

@NicoloRibaudo

87 of 233

The anatomy of a Babel plugin

function codemod({ types: t, template }) {

// ...

return {

visitor: {

// ...

}

};

}

87

Get different utilities from Babel, ...

… define helper functions to analyze and transform ...

… and intercept the AST nodes to handle.

@NicoloRibaudo

88 of 233

  1. Detecting React class components

class MyComponent extends React.Component {

// ...

}

88

@NicoloRibaudo

89 of 233

  • Detecting React class components

class MyComponent extends React.Component {

// ...

}

89

@NicoloRibaudo

90 of 233

  • Detecting React class components

90

@NicoloRibaudo

91 of 233

  • Detecting React class components

return {

visitor: {

// ...

}

};

91

@NicoloRibaudo

92 of 233

  • Detecting React class components

return {

visitor: {

ClassDeclaration(path) {

if (!isReactComponent(path.node)) {

return;

}

alert("We have a React component!");

}

}

};

92

@NicoloRibaudo

93 of 233

  • Detecting React class components

function isReactComponent(node) {

return t.matchesPattern(

node.superClass,

"React.Component"

);

}

return {

visitor: {

ClassDeclaration(path) { /* ... */ }

}

};

93

@NicoloRibaudo

94 of 233

  1. Extracting the render() method

94

@NicoloRibaudo

95 of 233

  • Extracting the render() method

class Counter extends Component {

render() {

return <p>It works!</p>;

}

}

95

@NicoloRibaudo

96 of 233

  • Extracting the render() method

class Counter extends Component {

render() {

return <p>It works!</p>;

}

}

96

@NicoloRibaudo

97 of 233

  • Extracting the render() method

function findMethod(node, name) {

}

findMethod(path.node, "render");

97

@NicoloRibaudo

98 of 233

  • Extracting the render() method

function findMethod(node, name) {

const elem = node.body.body

}

findMethod(path.node, "render");

98

@NicoloRibaudo

99 of 233

  • Extracting the render() method

function findMethod(node, name) {

const elem = node.body.body.find(el =>

t.isClassMethod(el)

);

}

findMethod(path.node, "render");

99

@NicoloRibaudo

100 of 233

  • Extracting the render() method

function findMethod(node, name) {

const elem = node.body.body.find(el =>

t.isClassMethod(el, {

static: false, computed: false

})

);

}

findMethod(path.node, "render");

100

@NicoloRibaudo

101 of 233

  • Extracting the render() method

function findMethod(node, name) {

const elem = node.body.body.find(el =>

t.isClassMethod(el, {

static: false, computed: false

})

&& t.isIdentifier(el.key, { name })

);

}

findMethod(path.node, "render");

101

@NicoloRibaudo

102 of 233

  • Extracting the render() method

function findMethod(node, name) {

const elem = node.body.body.find(el =>

t.isClassMethod(el, {

static: false, computed: false

})

&& t.isIdentifier(el.key, { name })

);

if (elem) return elem.body.body;

}

findMethod(path.node, "render");

102

@NicoloRibaudo

103 of 233

  • Extracting the render() method

function findMethod(node, name) {

const elem = node.body.body.find(el =>

t.isClassMethod(el, {

static: false, computed: false

})

&& t.isIdentifier(el.key, { name })

);

if (elem) return elem.body.body;

}

findMethod(path.node, "render");

103

@NicoloRibaudo

104 of 233

  • Extracting the render() method

visitor: {

ClassDeclaration(path) {

if (!isReactComponent(path.node)) return;

path.replaceWith(template.ast`

const ${path.node.id} = (props) => {

${findMethod(path.node, "render")};

};

`);

},

},

104

@NicoloRibaudo

105 of 233

  • Extracting the render() method

visitor: {

ClassDeclaration(path) {

if (!isReactComponent(path.node)) return;

path.replaceWith(template.ast`

const ${path.node.id} = (props) => {

${findMethod(path.node, "render")};

};

`);

},

},

105

class Counter extends Component {

render() {

return <p>It works!</p>;

}

}

@NicoloRibaudo

106 of 233

  • Extracting the render() method

visitor: {

ClassDeclaration(path) {

if (!isReactComponent(path.node)) return;

path.replaceWith(template.ast`

const ${path.node.id} = (props) => {

${findMethod(path.node, "render")};

};

`);

},

},

106

class Counter extends Component {

render() {

return <p>It works!</p>;

}

}

const Counter = (props) => {

return <p>It works!</p>;

};

@NicoloRibaudo

107 of 233

  • Rewriting this.props usage

class Counter extends Component {

render() {

return <p>Hi, {this.props.name}!</p>

}

}

107

@NicoloRibaudo

108 of 233

  • Rewriting this.props usage

class Counter extends Component {

render() {

return <p>Hi, {this.props.name}!</p>

}

}

108

@NicoloRibaudo

109 of 233

  • Rewriting this.props usage

class Counter extends Component {

render() {

return <p>Hi, {this.props.name}!</p>

}

}

109

@NicoloRibaudo

110 of 233

  • Rewriting this.props usage

function rewritePropsUsage(path) {

}

110

@NicoloRibaudo

111 of 233

  • Rewriting this.props usage

function rewritePropsUsage(path) {

path.traverse({

MemberExpression(path) {

},

});

}

111

@NicoloRibaudo

112 of 233

  • Rewriting this.props usage

function rewritePropsUsage(path) {

path.traverse({

MemberExpression(path) {

const { node } = path;

if (

!node.computed

) {

}

},

});

}

112

@NicoloRibaudo

113 of 233

  • Rewriting this.props usage

function rewritePropsUsage(path) {

path.traverse({

MemberExpression(path) {

const { node } = path;

if (

!node.computed &&

t.isThisExpression(node.object)

) {

}

},

});

}

113

@NicoloRibaudo

114 of 233

  • Rewriting this.props usage

function rewritePropsUsage(path) {

path.traverse({

MemberExpression(path) {

const { node } = path;

if (

!node.computed &&

t.isThisExpression(node.object) &&

t.isIdentifier(node.property, { name: "props" })

) {

}

},

});

}

114

@NicoloRibaudo

115 of 233

  • Rewriting this.props usage

function rewritePropsUsage(path) {

path.traverse({

MemberExpression(path) {

const { node } = path;

if (

!node.computed &&

t.isThisExpression(node.object) &&

t.isIdentifier(node.property, { name: "props" })

) {

path.replaceWith(t.identifier("props"));

}

},

});

}

115

@NicoloRibaudo

116 of 233

  • Rewriting this.props usage

visitor: {

ClassDeclaration(path) {

if (!isReactComponent(path.node)) return;

path.replaceWith(template.ast`

const ${path.node.id} = (props) => {

${findMethod(path.node, "render")};

};

`);

},

},

116

@NicoloRibaudo

117 of 233

  • Rewriting this.props usage

visitor: {

ClassDeclaration(path) {

if (!isReactComponent(path.node)) return;

rewritePropsUsage(path);

path.replaceWith(template.ast`

const ${path.node.id} = (props) => {

${findMethod(path.node, "render")};

};

`);

},

},

@NicoloRibaudo

118 of 233

  • Rewriting this.props usage

visitor: {

ClassDeclaration(path) {

if (!isReactComponent(path.node)) return;

rewritePropsUsage(path);

path.replaceWith(template.ast`

const ${path.node.id} = (props) => {

${findMethod(path.node, "render")};

};

`);

},

},

118

class Counter extends Component {

render() {

return <p>

Hi, {this.props.name}!

</p>;

}

}

@NicoloRibaudo

119 of 233

  • Rewriting this.props usage

visitor: {

ClassDeclaration(path) {

if (!isReactComponent(path.node)) return;

rewritePropsUsage(path);

path.replaceWith(template.ast`

const ${path.node.id} = (props) => {

${findMethod(path.node, "render")};

};

`);

},

},

119

class Counter extends Component {

render() {

return <p>

Hi, {this.props.name}!

</p>;

}

}

const Counter = (props) => {

return <p>Hi, {props.name}!</p>;

};

@NicoloRibaudo

120 of 233

  1. Convert state = {…} to useState()

class Counter extends Component {

state = { count: 0 };

render() {

return <p>

Total: {this.state.count}

</p>;

}

}

120

@NicoloRibaudo

121 of 233

  • Convert state = {…} to useState()

class Counter extends Component {

state = { count: 0 };

render() {

return <p>

Total: {this.state.count}

</p>;

}

}

121

@NicoloRibaudo

122 of 233

  • Convert state = {…} to useState()

class Counter extends Component {

state = { count: 0 };

render() {

return <p>

Total: {this.state.count}

</p>;

}

}

122

@NicoloRibaudo

123 of 233

  • Convert state = {…} to useState()

function findField(node, name) {

const elem = node.body.body.find(el =>

t.isClassProperty(el, {

static: false, computed: false

})

&& t.isIdentifier(el.key, { name })

);

if (elem) return elem.value;

}

findField(path.node, "state");

123

@NicoloRibaudo

124 of 233

  • Convert state = {…} to useState()

class Counter extends Component {

state = { count: 0 };

render() {

return <p>

Total: {this.state.count}

</p>;

}

}

124

count get : Identifier { "count" }

set : Identifier { "setCount" }

init : NumericLiteral { 0 }

...

@NicoloRibaudo

125 of 233

  • Convert state = {…} to useState()

125

@NicoloRibaudo

126 of 233

  • Convert state = {…} to useState()

function findInitialState(node) {

const stateNode = findField(node, "state");

if (!stateNode) return;

}

126

@NicoloRibaudo

127 of 233

  • Convert state = {…} to useState()

function findInitialState(node) {

const stateNode = findField(node, "state");

if (!stateNode) return;

const state = new Map();

for (const prop of stateNode.properties) {

}

return state;

}

127

@NicoloRibaudo

128 of 233

  • Convert state = {…} to useState()

function findInitialState(node) {

const stateNode = findField(node, "state");

if (!stateNode) return;

const state = new Map();

for (const prop of stateNode.properties) {

state.set(prop.key.name, {

get: t.identifier(prop.key.name),

set: t.identifier(`set${upper(prop.key.name)}`),

init: prop.value,

});

}

return state;

}

128

@NicoloRibaudo

129 of 233

  • Convert state = {…} to useState()

path.replaceWith(template.ast`

const ${componentId} = (props) => {

${findMethod(path, "render")};

};

`);

129

get : Identifier { … }

set : Identifier { … }

init : NumericLiteral { … }

...

@NicoloRibaudo

130 of 233

  • Convert state = {…} to useState()

const state = findInitialState(path.node);

path.replaceWith(template.ast`

const ${componentId} = (props) => {

${findMethod(path, "render")};

};

`);

130

get : Identifier { … }

set : Identifier { … }

init : NumericLiteral { … }

...

@NicoloRibaudo

131 of 233

  • Convert state = {…} to useState()

const state = findInitialState(path.node);

const useStateCalls = [];

for (let { get, set, init } of state.values())

useStateCalls.push(template.ast`

const [${get}, ${set}] = useState(${init})

`);

path.replaceWith(template.ast`

const ${componentId} = (props) => {

${findMethod(path, "render")};

};

`);

131

get : Identifier { … }

set : Identifier { … }

init : NumericLiteral { … }

...

@NicoloRibaudo

132 of 233

  • Convert state = {…} to useState()

const state = findInitialState(path.node);

const useStateCalls = [];

for (let { get, set, init } of state.values())

useStateCalls.push(template.ast`

const [${get}, ${set}] = useState(${init})

`);

path.replaceWith(template.ast`

const ${componentId} = (props) => {

${useStateCalls};

${findMethod(path, "render")};

};

`);

132

get : Identifier { … }

set : Identifier { … }

init : NumericLiteral { … }

...

@NicoloRibaudo

133 of 233

  • Convert state = {…} to useState()

const state = findInitialState(path.node);

const useStateCalls = [];

for (let { get, set, init } of state.values())

useStateCalls.push(template.ast`

const [${get}, ${set}] = useState(${init})

`);

path.replaceWith(template.ast`

const ${componentId} = (props) => {

${useStateCalls};

${findMethod(path, "render")};

};

`);

133

class Counter extends Component {

state = { count: 0 };

render() {

return /* ... */;

}

}

@NicoloRibaudo

134 of 233

  • Convert state = {…} to useState()

const state = findInitialState(path.node);

const useStateCalls = [];

for (let { get, set, init } of state.values())

useStateCalls.push(template.ast`

const [${get}, ${set}] = useState(${init})

`);

path.replaceWith(template.ast`

const ${componentId} = (props) => {

${useStateCalls};

${findMethod(path, "render")};

};

`);

134

class Counter extends Component {

state = { count: 0 };

render() {

return /* ... */;

}

}

const Counter = (props) => {

const [count, setCount] = useState(0);

return /* ... */;

};

@NicoloRibaudo

135 of 233

  • Convert this.state.count to count

135

class Counter extends Component {

state = { count: 0 };

render() {

return <p>Total: {this.state.count}</p>;

}

}

const Counter = (props) => {

const [count, setCount] = useState(0);

return <p>Total: {count}</p>;

};

@NicoloRibaudo

136 of 233

  • Convert this.setState to setCount

136

render() {

const inc = () => this.setState(prev => ({

count: prev.count + 1

}));

return <p onClick={inc}>Total: {this.state.count}</p>;

}

const Counter = (props) => {

const [count, setCount] = useState(0);

const inc = () => setCount(count => count + 1);

return <p onClick={inc}>Total: {count}</p>;

};

@NicoloRibaudo

137 of 233

< >

137

@NicoloRibaudo

138 of 233

< >

Does our codemod cover every case?

138

@NicoloRibaudo

139 of 233

Ignoring unsupported patterns

139

@NicoloRibaudo

140 of 233

Ignoring unsupported patterns

Complex state initialization

140

@NicoloRibaudo

141 of 233

Complex state initialization

141

class Counter extends Component {

state = { count: 0, label: "Likes" };

render() {

return <p>

{this.state.label}: {this.state.count}

</p>;

}

}

@NicoloRibaudo

142 of 233

Complex state initialization

142

class Counter extends Component {

state = { count: 0, label: "Likes" };

render() {

return <p>

{this.state.label}: {this.state.count}

</p>;

}

}

const Counter = (props) => {

const [count, setCount] = useState(0);

const [label, setLabel] = useState("Likes");

return <p>{label}: {count}</p>;

};

@NicoloRibaudo

143 of 233

Complex state initialization

143

class Counter extends Component {

state = getInitialState();

render() {

return <p>

{this.state.label}: {this.state.count}

</p>;

}

}

const Counter = (props) => {

const [count, setCount] = ??????;

const [label, setLabel] = ??????;

return <p>{label}: {count}</p>;

};

@NicoloRibaudo

144 of 233

Complex state initialization

function findInitialState(node) {

const stateNode = findField(node, "state");

if (!stateNode) return;

const state = new Map();

for (const prop of stateNode.properties) {

/* ... */

}

return state;

}

144

@NicoloRibaudo

145 of 233

Complex state initialization

function findInitialState(node) {

const stateNode = findField(node, "state");

if (!stateNode) return;

const state = new Map();

for (const prop of stateNode.properties) {

/* ... */

}

return state;

}

145

@NicoloRibaudo

146 of 233

Complex state initialization

function findInitialState(node) {

const stateNode = findField(node, "state");

if (!stateNode) return;

if (!t.isObjectExpression(stateNode)) {

return;

}

const state = new Map();

for (const prop of stateNode.properties) {

/* ... */

146

@NicoloRibaudo

147 of 233

Complex state initialization

function findInitialState(node) {

const stateNode = findField(node, "state");

if (!stateNode) return;

if (!t.isObjectExpression(stateNode)) {

t.addComment(

stateNode.node, "leading", " @warning: Unable to refactor complex state initialization ",

);

return;

}

const state = new Map();

for (const prop of stateNode.properties) {

/* ... */

147

@NicoloRibaudo

148 of 233

Complex state initialization

148

class Counter extends Component {

state = getInisialState();

render() {

return <p>

{this.state.label}: {this.state.count}

</p>;

}

}

@NicoloRibaudo

149 of 233

Complex state initialization

149

class Counter extends Component {

state = getInisialState();

render() {

return <p>

{this.state.label}: {this.state.count}

</p>;

}

}

class Counter extends Component {

state =

/* @warning: Unable to refactor complex state initialization */

getInisialState();

render() {

return <p>

{this.state.label}: {this.state.count}

</p>;

}

}

@NicoloRibaudo

150 of 233

Analyzing a pattern's frequency

150

@NicoloRibaudo

151 of 233

Analyzing a pattern's frequency

Is it worth to implement support for defaultProps?

151

class Counter extends Component {

static defaultProps = { name: "Nicolò" };

render() {

return <p>Hi, {this.props.name}!</p>;

}

}

@NicoloRibaudo

152 of 233

Analyzing a pattern's frequency

152

class Person {

constructor(name) {

this.name = name;

}

}

class Btn extends Component {

render() {

return <button />

}

}

class Btn extends Component {

static defaultProps = {

name: "Test",

};

render() {

return <button />

}

}

@NicoloRibaudo

153 of 233

Analyzing a pattern's frequency

let reactClasses = 0, defaultProps = 0;

function defaultPropsAnalysis({ types: t }) {

return {

visitor: {

// ...

},

};

153

class Person {

constructor(name) {

this.name = name;

}

}

class Btn extends Component {

render() {

return <button />

}

}

class Btn extends Component {

static defaultProps = {

name: "Test",

};

render() {

return <button />

}

}

@NicoloRibaudo

154 of 233

Analyzing a pattern's frequency

let reactClasses = 0, defaultProps = 0;

function defaultPropsAnalysis({ types: t }) {

return {

visitor: {

ClassDeclaration(path) {

// ...

}

},

};

154

class Person {

constructor(name) {

this.name = name;

}

}

class Btn extends Component {

render() {

return <button />

}

}

class Btn extends Component {

static defaultProps = {

name: "Test",

};

render() {

return <button />

}

}

@NicoloRibaudo

155 of 233

Analyzing a pattern's frequency

let reactClasses = 2, defaultProps = 0;

function defaultPropsAnalysis({ types: t }) {

return {

visitor: {

ClassDeclaration(path) {

if (!isReactComponent(path.node)) return;

reactClasses++;

// ...

}

},

};

155

class Person {

constructor(name) {

this.name = name;

}

}

class Btn extends Component {

render() {

return <button />

}

}

class Btn extends Component {

static defaultProps = {

name: "Test",

};

render() {

return <button />

}

}

@NicoloRibaudo

156 of 233

Analyzing a pattern's frequency

let reactClasses = 2, defaultProps = 1;

function defaultPropsAnalysis({ types: t }) {

return {

visitor: {

ClassDeclaration(path) {

if (!isReactComponent(path.node)) return;

reactClasses++;

if (findField(path.node, "defaultProps"))

defaultProps++;

}

},

};

156

class Person {

constructor(name) {

this.name = name;

}

}

class Btn extends Component {

render() {

return <button />

}

}

class Btn extends Component {

static defaultProps = {

name: "Test",

};

render() {

return <button />

}

}

@NicoloRibaudo

157 of 233

Analyzing a pattern's frequency

glob("src/**/*.jsx").forEach(filename => {

});

157

class Person {

constructor(name) {

this.name = name;

}

}

class Btn extends Component {

render() {

return <button />

}

}

class Btn extends Component {

static defaultProps = {

name: "Test",

};

render() {

return <button />

}

}

@NicoloRibaudo

158 of 233

Analyzing a pattern's frequency

glob("src/**/*.jsx").forEach(filename => {

babel.transformFileSync(filename, {

plugins: [defaultPropsAnalysis]

});

});

158

class Person {

constructor(name) {

this.name = name;

}

}

class Btn extends Component {

render() {

return <button />

}

}

class Btn extends Component {

static defaultProps = {

name: "Test",

};

render() {

return <button />

}

}

@NicoloRibaudo

159 of 233

Analyzing a pattern's frequency

glob("src/**/*.jsx").forEach(filename => {

babel.transformFileSync(filename, {

plugins: [defaultPropsAnalysis]

});

});

console.log(`

Number of React classes: ${reactClasses}

Number ot classes using defaultProps: ${defaultProps}

(${(defaultProps / reactClasses)})

`);

159

class Person {

constructor(name) {

this.name = name;

}

}

class Btn extends Component {

render() {

return <button />

}

}

class Btn extends Component {

static defaultProps = {

name: "Test",

};

render() {

return <button />

}

}

@NicoloRibaudo

160 of 233

Analyzing a pattern's frequency

glob("src/**/*.jsx").forEach(filename => {

babel.transformFileSync(filename, {

plugins: [defaultPropsAnalysis]

});

});

console.log(`

Number of React classes: ${reactClasses}

Number ot classes using defaultProps: ${defaultProps}

(${(defaultProps / reactClasses)})

`);

160

class Person {

constructor(name) {

this.name = name;

}

}

class Btn extends Component {

render() {

return <button />

}

}

class Btn extends Component {

static defaultProps = {

name: "Test",

};

render() {

return <button />

}

}

Number of React classes: 150

Number ot classes using defaultProps: 5

(0.03333333333333333)

@NicoloRibaudo

161 of 233

</ >

161

@NicoloRibaudo

162 of 233

162

@NicoloRibaudo

163 of 233

Back to our codemod ...

163

@NicoloRibaudo

164 of 233

Back to our codemod ...

… assuming that we run some analysis and discovered that we almost only use arrow functions instead of class methods

164

@NicoloRibaudo

165 of 233

  • Convert class fields to variables

class Counter extends Component {

state = { count: 0 };

inc = () =>

this.setState(state => ({

count: state.count + this.props.step,

}));

render() {

return <p onClick={this.inc}>

Total: {this.state.count}

</p>;

}

}

165

@NicoloRibaudo

166 of 233

  • Convert class fields to variables

class Counter extends Component {

state = { count: 0 };

inc = () =>

this.setState(state => ({

count: state.count + this.props.step,

}));

render() {

return <p onClick={this.inc}>

Total: {this.state.count}

</p>;

}

}

166

@NicoloRibaudo

167 of 233

  • Convert class fields to variables

class Counter extends Component {

state = { count: 0 };

inc = () =>

this.setState(state => ({

count: state.count + this.props.step,

}));

render() {

return <p onClick={this.inc}>

Total: {this.state.count}

</p>;

}

}

167

state init : ObjectLiteral { ... }

inc init : ArrowFunctionExpression {}

...

@NicoloRibaudo

168 of 233

  • Convert class fields to variables

class Counter extends Component {

state = { count: 0 };

inc = () =>

this.setState(state => ({

count: state.count + this.props.step,

}));

render() {

return <p onClick={this.inc}>

Total: {this.state.count}

</p>;

}

}

168

inc init : ArrowFunctionExpression {}

...

@NicoloRibaudo

169 of 233

  • Convert class fields to variables

function findClassProperties(node) {

const properties = new Map();

return properties;

}

169

@NicoloRibaudo

170 of 233

  • Convert class fields to variables

function findClassProperties(node) {

const properties = new Map();

for (const elem of node.body.body) {

}

return properties;

}

170

@NicoloRibaudo

171 of 233

  • Convert class fields to variables

function findClassProperties(node) {

const properties = new Map();

for (const elem of node.body.body) {

if (

!t.isClassProperty(elem, {

computed: false, static: false,

})

) continue;

}

return properties;

}

171

@NicoloRibaudo

172 of 233

  • Convert class fields to variables

function findClassProperties(node) {

const properties = new Map();

for (const elem of node.body.body) {

if (

!t.isClassProperty(elem, {

computed: false, static: false,

})

) continue;

if (elem.key.name === "state") continue;

}

return properties;

}

172

@NicoloRibaudo

173 of 233

  • Convert class fields to variables

function findClassProperties(node) {

const properties = new Map();

for (const elem of node.body.body) {

if (

!t.isClassProperty(elem, {

computed: false, static: false,

})

) continue;

if (elem.key.name === "state") continue;

properties.set(elem.key.name, elem.value);

}

return properties;

}

173

@NicoloRibaudo

174 of 233

  • Convert class fields to variables

visotor: {

ClassDeclaration(path) {

// ...

path.replaceWith(template.ast`

const ${componentId} = (props) => {

${useStateCalls};

${findMethod(path, "render")};

};

`);

}

}

174

@NicoloRibaudo

175 of 233

  • Convert class fields to variables

visotor: {

ClassDeclaration(path) {

// ...

path.replaceWith(template.ast`

const ${componentId} = (props) => {

${useStateCalls};

${findMethod(path, "render")};

};

`);

}

}

175

inc init : ArrowFunctionExpression {}

...

@NicoloRibaudo

176 of 233

  • Convert class fields to variables

// ...

path.replaceWith(template.ast`

const ${componentId} = (props) => {

${useStateCalls};

${findMethod(path, "render")};

};

`);

176

inc init : ArrowFunctionExpression {}

...

@NicoloRibaudo

177 of 233

  • Convert class fields to variables

const vars = findClassProperties(path.node);

// ...

path.replaceWith(template.ast`

const ${componentId} = (props) => {

${useStateCalls};

${findMethod(path, "render")};

};

`);

177

inc init : ArrowFunctionExpression {}

...

@NicoloRibaudo

178 of 233

  • Convert class fields to variables

const vars = findClassProperties(path.node);

const varsDeclarations = [];

for (const [name, init] of vars)

varsDeclarations.push(template.ast`

const ${t.identifier(name)} = ${init};

`);

path.replaceWith(template.ast`

const ${componentId} = (props) => {

${useStateCalls};

${findMethod(path, "render")};

};

`);

178

inc init : ArrowFunctionExpression {}

...

@NicoloRibaudo

179 of 233

  • Convert class fields to variables

const vars = findClassProperties(path.node);

const varsDeclarations = [];

for (const [name, init] of vars)

varsDeclarations.push(template.ast`

const ${t.identifier(name)} = ${init};

`);

path.replaceWith(template.ast`

const ${componentId} = (props) => {

${useStateCalls};

${varsDeclarations};

${findMethod(path, "render")};

};

`);

179

inc init : ArrowFunctionExpression {}

...

@NicoloRibaudo

180 of 233

  • Convert class fields to variables

180

class Counter extends Component {

state = { count: 0 };

inc = () => this.setState(state => ({

count: state.count + this.props.step,

}));

render() {

return <p onClick={this.inc}>

Total: {this.state.count}

</p>;

}

}

@NicoloRibaudo

181 of 233

  • Convert class fields to variables

181

class Counter extends Component {

state = { count: 0 };

inc = () => this.setState(state => ({

count: state.count + this.props.step,

}));

render() {

return <p onClick={this.inc}>

Total: {this.state.count}

</p>;

}

}

const Counter = props => {

const [count, setCount] = useState(0);

const inc = () => setCount(

count => count + props.step

);

return <p onClick={this.inc}>

Total: {count}

</p>;

};

@NicoloRibaudo

182 of 233

  • Convert class fields to variables

182

class Counter extends Component {

state = { count: 0 };

inc = () => this.setState(state => ({

count: state.count + this.props.step,

}));

render() {

return <p onClick={this.inc}>

Total: {this.state.count}

</p>;

}

}

const Counter = props => {

const [count, setCount] = useState(0);

const inc = () => setCount(

count => count + props.step

);

return <p onClick={this.inc}>

Total: {count}

</p>;

};

@NicoloRibaudo

183 of 233

  • Convert class fields to variables

183

class Counter extends Component {

state = { count: 0 };

inc = () => this.setState(state => ({

count: state.count + this.props.step,

}));

render() {

return <p onClick={this.inc}>

Total: {this.state.count}

</p>;

}

}

@NicoloRibaudo

184 of 233

  • Convert class fields usage to variables

function rewriteVarsUsage(path, props) {

}

184

@NicoloRibaudo

185 of 233

  • Convert class fields usage to variables

function rewriteVarsUsage(path, props) {

path.traverse({

MemberExpression(path) {

}

});

}

185

@NicoloRibaudo

186 of 233

  • Convert class fields usage to variables

function rewriteVarsUsage(path, props) {

path.traverse({

MemberExpression(path) {

const { node } = path;

if (!t.isThisExpression(node.object)) return;

if (node.computed) return;

}

});

}

186

@NicoloRibaudo

187 of 233

  • Convert class fields usage to variables

function rewriteVarsUsage(path, props) {

path.traverse({

MemberExpression(path) {

const { node } = path;

if (!t.isThisExpression(node.object)) return;

if (node.computed) return;

const { name } = node.property;

if (!props.has(name)) return;

}

});

}

187

@NicoloRibaudo

188 of 233

  • Convert class fields usage to variables

function rewriteVarsUsage(path, props) {

path.traverse({

MemberExpression(path) {

const { node } = path;

if (!t.isThisExpression(node.object)) return;

if (node.computed) return;

const { name } = node.property;

if (!props.has(name)) return;

path.replaceWith(t.identifier(name));

}

});

}

188

@NicoloRibaudo

189 of 233

  • Convert class fields usage to variables

189

class Counter extends Component {

state = { count: 0 };

inc = () => this.setState(state => ({

count: state.count + this.props.step,

}));

render() {

return <p onClick={this.inc}>

Total: {this.state.count}

</p>;

}

}

const Counter = props => {

const [count, setCount] = useState(0);

const inc = () => setCount(

count => count + props.step

);

return <p onClick={inc}>

Total: {count}

</p>;

};

@NicoloRibaudo

190 of 233

  • Inject imports for used hooks

190

@NicoloRibaudo

191 of 233

  • Inject imports for used hooks

So far we only implemented local transforms, modifying a self-contained AST node:

ClassDeclaration

191

@NicoloRibaudo

192 of 233

  • Inject imports for used hooks

So far we only implemented local transforms, modifying a self-contained AST node:

ClassDeclaration

We can use Babel's scope analysis and AST utilities to modify unrelated parts of the AST.

192

@NicoloRibaudo

193 of 233

  • Inject imports for used hooks

193

class Counter extends Component {

state = { count: 0 };

render() {

return <p>Total: {this.state.count}</p>;

}

}

const Counter = (props) => {

const [count, setCount] = useState(0);

return <p>Total: {count}</p>;

};

@NicoloRibaudo

194 of 233

  • Inject imports for used hooks

194

class Counter extends Component {

state = { count: 0 };

render() {

return <p>Total: {this.state.count}</p>;

}

}

const Counter = (props) => {

const [count, setCount] = useState(0);

return <p>Total: {count}</p>;

};

ReferenceError: useState is not defined

@NicoloRibaudo

195 of 233

  • Inject imports for used hooks

195

class Counter extends Component {

state = { count: 0 };

render() {

return <p>Total: {this.state.count}</p>;

}

}

const Counter = (props) => {

const [count, setCount] = useState(0);

return <p>Total: {count}</p>;

};

@NicoloRibaudo

196 of 233

  • Inject imports for used hooks

196

class Counter extends Component {

state = { count: 0 };

render() {

return <p>Total: {this.state.count}</p>;

}

}

import { useState } from "react";

const Counter = (props) => {

const [count, setCount] = useState(0);

return <p>Total: {count}</p>;

};

@NicoloRibaudo

197 of 233

  • Inject imports for used hooks

function findReactImport(path) {

}

197

import * as _ from "lodash";

import React, { Component } from "react";

export function buildBtn(color) {

class Btn extends Component {

state = { x: 0 };

render() {

return (

<button color={color} />

);

}

}

return Btn;

}

@NicoloRibaudo

198 of 233

  • Inject imports for used hooks

function findReactImport(path) {

const program = path.findParent((p) => p.isProgram());

}

198

import * as _ from "lodash";

import React, { Component } from "react";

export function buildBtn(color) {

class Btn extends Component {

state = { x: 0 };

render() {

return (

<button color={color} />

);

}

}

return Btn;

}

@NicoloRibaudo

199 of 233

  • Inject imports for used hooks

function findReactImport(path) {

const program = path.findParent((p) => p.isProgram());

let importPath = program.get("body")

.filter(({ node }) => t.isImportDeclaration(node))

}

199

import * as _ from "lodash";

import React, { Component } from "react";

export function buildBtn(color) {

class Btn extends Component {

state = { x: 0 };

render() {

return (

<button color={color} />

);

}

}

return Btn;

}

@NicoloRibaudo

200 of 233

  • Inject imports for used hooks

function findReactImport(path) {

const program = path.findParent((p) => p.isProgram());

let importPath = program.get("body")

.filter(({ node }) => t.isImportDeclaration(node))

.find(({ node }) => node.source.value === "react");

}

200

import * as _ from "lodash";

import React, { Component } from "react";

export function buildBtn(color) {

class Btn extends Component {

state = { x: 0 };

render() {

return (

<button color={color} />

);

}

}

return Btn;

}

@NicoloRibaudo

201 of 233

  • Inject imports for used hooks

function findReactImport(path) {

const program = path.findParent((p) => p.isProgram());

let importPath = program.get("body")

.filter(({ node }) => t.isImportDeclaration(node))

.find(({ node }) => node.source.value === "react");

if (importPath) return importPath;

}

201

import * as _ from "lodash";

import React, { Component } from "react";

export function buildBtn(color) {

class Btn extends Component {

state = { x: 0 };

render() {

return (

<button color={color} />

);

}

}

return Btn;

}

@NicoloRibaudo

202 of 233

  • Inject imports for used hooks

function findReactImport(path) {

const program = path.findParent((p) => p.isProgram());

let importPath = program.get("body")

.filter(({ node }) => t.isImportDeclaration(node))

.find(({ node }) => node.source.value === "react");

if (importPath) return importPath;

}

202

import * as _ from "lodash";

export function buildBtn(color) {

class Btn extends Component {

state = { x: 0 };

render() {

return (

<button color={color} />

);

}

}

return Btn;

}

@NicoloRibaudo

203 of 233

  • Inject imports for used hooks

function findReactImport(path) {

const program = path.findParent((p) => p.isProgram());

let importPath = program.get("body")

.filter(({ node }) => t.isImportDeclaration(node))

.find(({ node }) => node.source.value === "react");

if (importPath) return importPath;

program.unshiftContainer(

"body", template.ast`import "react"`

);

}

203

import "react";

import * as _ from "lodash";

export function buildBtn(color) {

class Btn extends Component {

state = { x: 0 };

render() {

return (

<button color={color} />

);

}

}

return Btn;

}

@NicoloRibaudo

204 of 233

  • Inject imports for used hooks

function findReactImport(path) {

const program = path.findParent((p) => p.isProgram());

let importPath = program.get("body")

.filter(({ node }) => t.isImportDeclaration(node))

.find(({ node }) => node.source.value === "react");

if (importPath) return importPath;

program.unshiftContainer(

"body", template.ast`import "react"`

);

return program.get("body.0");

}

204

import "react";

import * as _ from "lodash";

export function buildBtn(color) {

class Btn extends Component {

state = { x: 0 };

render() {

return (

<button color={color} />

);

}

}

return Btn;

}

@NicoloRibaudo

205 of 233

  • Inject imports for used hooks

function findReactImport(path) {

const program = path.findParent((p) => p.isProgram());

let importPath = program.get("body")

.filter(({ node }) => t.isImportDeclaration(node))

.find(({ node }) => node.source.value === "react");

if (importPath) return importPath;

program.unshiftContainer(

"body", template.ast`import "react"`

);

return program.get("body.0");

}

205

@NicoloRibaudo

206 of 233

  • Inject imports for used hooks

function findReactImport(path) {

const program = path.findParent((p) => p.isProgram());

let importPath = program.get("body")

.filter(({ node }) => t.isImportDeclaration(node))

.find(({ node }) => node.source.value === "react");

if (importPath) return importPath;

program.unshiftContainer(

"body", template.ast`import "react"`

);

return program.get("body.0");

}

206

We are always working with paths, not with nodes.

@NicoloRibaudo

207 of 233

  • Inject imports for used hooks

function findReactImport(path) {

const program = path.findParent((p) => p.isProgram());

let importPath = program.get("body")

.filter(({ node }) => t.isImportDeclaration(node))

.find(({ node }) => node.source.value === "react");

if (importPath) return importPath;

program.unshiftContainer(

"body", template.ast`import "react"`

);

return program.get("body.0");

}

207

We are always working with paths, not with nodes.

AST nodes are ergonomic for reading the AST, but when mutating it we must use NodePaths to let Babel

track updates.

@NicoloRibaudo

208 of 233

  • Inject imports for used hooks

visotor: {

ClassDeclaration(path) {

/* ... */

}

}

208

import "react";

import * as _ from "lodash";

export function buildBtn(color) {

class Btn extends Component {

state = { x: 0 };

render() {

return (

<button color={color} />

);

}

}

return Btn;

}

@NicoloRibaudo

209 of 233

  • Inject imports for used hooks

visotor: {

ClassDeclaration(path) {

/* ... */

}

}

209

import "react";

import * as _ from "lodash";

export function buildBtn(color) {

const Btn = props => {

const [x, setX] = useState(0);

return (

<button color={color} />

);

}

return Btn;

}

@NicoloRibaudo

210 of 233

  • Inject imports for used hooks

visotor: {

ClassDeclaration(path) {

/* ... */

if (

useStateCalls.length > 0

) {

}

}

}

210

import "react";

import * as _ from "lodash";

export function buildBtn(color) {

const Btn = props => {

const [x, setX] = useState(0);

return (

<button color={color} />

);

}

return Btn;

}

@NicoloRibaudo

211 of 233

  • Inject imports for used hooks

visotor: {

ClassDeclaration(path) {

/* ... */

if (

useStateCalls.length > 0

) {

findReactImport(path)

}

}

}

211

import "react";

import * as _ from "lodash";

export function buildBtn(color) {

const Btn = props => {

const [x, setX] = useState(0);

return (

<button color={color} />

);

}

return Btn;

}

@NicoloRibaudo

212 of 233

  • Inject imports for used hooks

visotor: {

ClassDeclaration(path) {

/* ... */

if (

useStateCalls.length > 0

) {

findReactImport(path)

}

}

}

212

import { useState } from "react";

@NicoloRibaudo

213 of 233

  • Inject imports for used hooks

visotor: {

ClassDeclaration(path) {

/* ... */

if (

useStateCalls.length > 0

) {

findReactImport(path)

}

}

}

213

import { useState } from "react";

@NicoloRibaudo

214 of 233

  • Inject imports for used hooks

visotor: {

ClassDeclaration(path) {

/* ... */

if (

useStateCalls.length > 0

) {

findReactImport(path)

}

}

}

214

import { useState as useState } from "react";

@NicoloRibaudo

215 of 233

  • Inject imports for used hooks

visotor: {

ClassDeclaration(path) {

/* ... */

if (

useStateCalls.length > 0

) {

findReactImport(path)

}

}

}

215

import "react";

import * as _ from "lodash";

export function buildBtn(color) {

const Btn = props => {

const [x, setX] = useState(0);

return (

<button color={color} />

);

}

return Btn;

}

@NicoloRibaudo

216 of 233

  • Inject imports for used hooks

visotor: {

ClassDeclaration(path) {

/* ... */

if (

useStateCalls.length > 0

) {

findReactImport(path).pushContainer(

"specifiers",

t.importSpecifier(t.identifier("useState"),

t.identifier("useState"))

);

}

}

}

216

import "react";

import * as _ from "lodash";

export function buildBtn(color) {

const Btn = props => {

const [x, setX] = useState(0);

return (

<button color={color} />

);

}

return Btn;

}

@NicoloRibaudo

217 of 233

  • Inject imports for used hooks

visotor: {

ClassDeclaration(path) {

/* ... */

if (

useStateCalls.length > 0 &&

!path.scope.hasBinding("useState")

) {

findReactImport(path).pushContainer(

"specifiers",

t.importSpecifier(t.identifier("useState"),

t.identifier("useState"))

);

}

}

}

217

import "react";

import * as _ from "lodash";

export function buildBtn(color) {

const Btn = props => {

const [x, setX] = useState(0);

return (

<button color={color} />

);

}

return Btn;

}

@NicoloRibaudo

218 of 233

  • Inject imports for used hooks

218

class Counter extends Component {

state = { count: 0 };

render() {

return <p>Total: {this.state.count}</p>;

}

}

import { useState } from "react";

const Counter = (props) => {

const [count, setCount] = useState(0);

return <p>Total: {count}</p>;

};

@NicoloRibaudo

219 of 233

</Example>

219

@NicoloRibaudo

220 of 233

Complementary tools

220

@NicoloRibaudo

221 of 233

Complementary tools

Babel is great at doing two things:

221

@NicoloRibaudo

222 of 233

Complementary tools

Babel is great at doing two things:

  • Parsing JavaScript, proposals or language extensions

222

@NicoloRibaudo

223 of 233

Complementary tools

Babel is great at doing two things:

  • Parsing JavaScript, proposals or language extensions
  • Providing tools to transform the code's AST

223

@NicoloRibaudo

224 of 233

Complementary tools

Babel is great at doing two things:

  • Parsing JavaScript, proposals or language extensions
  • Providing tools to transform the code's AST

Codemods also need:

224

@NicoloRibaudo

225 of 233

Complementary tools

Babel is great at doing two things:

  • Parsing JavaScript, proposals or language extensions
  • Providing tools to transform the code's AST

Codemods also need:

  • A way to print the resulting AST, keeping the original formatting

225

@NicoloRibaudo

226 of 233

Complementary tools

Babel is great at doing two things:

  • Parsing JavaScript, proposals or language extensions
  • Providing tools to transform the code's AST

Codemods also need:

  • A way to print the resulting AST, keeping the original formatting
  • A way to run the Babel plugin on the different files

226

@NicoloRibaudo

227 of 233

Complementary tools

Codemods also need:

  • A way to print the resulting AST, keeping the original formatting
  • A way to run the Babel plugin on the different files

227

@NicoloRibaudo

228 of 233

Complementary tools

Codemods also need:

  • A way to print the resulting AST, keeping the original formatting
  • A way to run the Babel plugin on the different files

228

@NicoloRibaudo

229 of 233

Complementary tools

Codemods also need:

  • A way to print the resulting AST, keeping the original formatting
  • A way to run the Babel plugin on the different files

229

recast

@NicoloRibaudo

230 of 233

Complementary tools

Codemods also need:

  • A way to print the resulting AST, keeping the original formatting
  • A way to run the Babel plugin on the different files

230

recast

JSCodeshift's CLI

@NicoloRibaudo

231 of 233

Complementary tools

Codemods also need:

  • A way to print the resulting AST, keeping the original formatting
  • A way to run the Babel plugin on the different files

231

recast

JSCodeshift's CLI

A manual for loop that iterates over the files

@NicoloRibaudo

232 of 233

Demo!

232

@NicoloRibaudo

233 of 233

Babel:�A refactoring tool

233

NICOLÒ RIBAUDO

Babel team

@nicolo-ribaudo

@NicoloRibaudo

nicolo.ribaudo@gmail.com

@NicoloRibaudo