Babel:�A refactoring tool
@NicoloRibaudo
2
NICOLÒ RIBAUDO
Babel team
@nicolo-ribaudo
@NicoloRibaudo
nicolo.ribaudo@gmail.com
@NicoloRibaudo
3
@babel
@babeljs
https://babeljs.io
https://opencollective.com/babel
@NicoloRibaudo
What is Babel?
4
@NicoloRibaudo
What is Babel?
Babel is a JavaScript compiler
5
@NicoloRibaudo
It’s a JavaScript to JavaScript compiler
player.level ??= 70_000;
(_player$level = player.level) != null
? _player$level
: player.level = 70000;
6
@NicoloRibaudo
7
@NicoloRibaudo
8
@babel/how-to
@NicoloRibaudo
What is Babel?
9
9
@NicoloRibaudo
What is Babel?
Babel is a customizable JavaScript compiler
10
@NicoloRibaudo
So… what can I do?
11
@NicoloRibaudo
So… what can I do?
12
@NicoloRibaudo
So… what can I do?
13
@NicoloRibaudo
So… what can I do?
14
@NicoloRibaudo
So… what can I do?
15
@NicoloRibaudo
So… what can I do?
16
@NicoloRibaudo
So… what can I do?
17
@NicoloRibaudo
Codemods
One-time transforms and automate refactorings
18
@NicoloRibaudo
Codemods
19
@NicoloRibaudo
Codemods
Refactors… happen.
20
@NicoloRibaudo
Codemods
Refactors… happen.
21
Sometimes ... | And they take ... |
… they are self-contained, without impacting other parts or your program | … a few hours |
@NicoloRibaudo
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
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
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
What can you use codemods for?
25
@NicoloRibaudo
What can you use codemods for?
What did we use them for in Babel?
26
@NicoloRibaudo
What can you use codemods for?
What did we use them for in Babel?
27
@NicoloRibaudo
What can you use codemods for?
What did we use them for in Babel?
28
@NicoloRibaudo
What can you use codemods for?
What did we use them for in Babel?
29
@NicoloRibaudo
What can you use codemods for?
What did we use them for in Babel?
30
@NicoloRibaudo
What can you use codemods for?
What other companies use them for?
31
@NicoloRibaudo
What can you use codemods for?
What other companies use them for?
32
@NicoloRibaudo
What can you use codemods for?
What other companies use them for?
33
@NicoloRibaudo
What can you use codemods for?
What other companies use them for?
34
@NicoloRibaudo
Compilers vs Codemods
35
@NicoloRibaudo
Compilers vs Codemods
36
@NicoloRibaudo
Compilers vs Codemods
The usual Babel transforms are based on strictly defined semantics
37
@NicoloRibaudo
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
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
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
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
Compilers vs Codemods
Codemods are based on assumptions about your coding style
The usual Babel transforms are based on strictly defined semantics
42
@NicoloRibaudo
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
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
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
Human-assisted automated transforms
46
Why don't we just create the perfect codemod?
@NicoloRibaudo
Human-assisted automated transforms
47
@NicoloRibaudo
Human-assisted automated transforms
const fs = require("fs");
const { join } = require("path");
48
@NicoloRibaudo
Human-assisted automated transforms
const fs = require("fs");
const { join } = require("path");
import fs from "fs";
import { join } from "path";
49
@NicoloRibaudo
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
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
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
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
Human-assisted automated transforms
54
@NicoloRibaudo
Alternatives
55
@NicoloRibaudo
Alternatives
Regular Expressions
Good for simple, self-contained refactors
56
@NicoloRibaudo
Alternatives
JSCodeshift
The most popular tool to build JavaScript codemods
57
@NicoloRibaudo
Alternatives
JSCodeshift
The most popular tool to build JavaScript codemods
58
@NicoloRibaudo
Alternatives
JSCodeshift
The most popular tool to build JavaScript codemods
59
@NicoloRibaudo
Alternatives
JSCodeshift
The most popular tool to build JavaScript codemods
60
@NicoloRibaudo
Alternatives
JSCodeshift
The most popular tool to build JavaScript codemods
So… why Babel?
61
@NicoloRibaudo
Alternatives
JSCodeshift
The most popular tool to build JavaScript codemods
So… why Babel?
62
@NicoloRibaudo
Alternatives
JSCodeshift
The most popular tool to build JavaScript codemods
So… why Babel?
63
@NicoloRibaudo
A practical example
64
@NicoloRibaudo
A practical example
Transforming React class components to functions with hooks
65
@NicoloRibaudo
66
@NicoloRibaudo
67
React class components are still supported and won't go away soon.
@NicoloRibaudo
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
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
@NicoloRibaudo
AST Explorer
A plugin development playground
71
@NicoloRibaudo
AST Explorer: a plugin playground
72
@NicoloRibaudo
@NicoloRibaudo
AST Explorer: a plugin playground
73
@NicoloRibaudo
Input code
Input AST
Babel plugin
Output code
@NicoloRibaudo
74
@NicoloRibaudo
Steps
75
@NicoloRibaudo
Steps
76
Complexity
@NicoloRibaudo
Steps
77
Complexity
@NicoloRibaudo
Steps
78
Complexity
@NicoloRibaudo
Steps
79
Complexity
@NicoloRibaudo
Steps
80
Complexity
@NicoloRibaudo
Steps
81
Complexity
@NicoloRibaudo
Steps
82
Complexity
@NicoloRibaudo
Steps
83
Complexity
@NicoloRibaudo
The anatomy of a Babel plugin
function codemod({ types: t, template }) {
// ...
return {
visitor: {
// ...
}
};
}
84
@NicoloRibaudo
The anatomy of a Babel plugin
function codemod({ types: t, template }) {
// ...
return {
visitor: {
// ...
}
};
}
85
Get different utilities from Babel, ...
@NicoloRibaudo
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
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
class MyComponent extends React.Component {
// ...
}
88
@NicoloRibaudo
class MyComponent extends React.Component {
// ...
}
89
@NicoloRibaudo
90
@NicoloRibaudo
return {
visitor: {
// ...
}
};
91
@NicoloRibaudo
return {
visitor: {
ClassDeclaration(path) {
if (!isReactComponent(path.node)) {
return;
}
alert("We have a React component!");
}
}
};
92
@NicoloRibaudo
function isReactComponent(node) {
return t.matchesPattern(
node.superClass,
"React.Component"
);
}
return {
visitor: {
ClassDeclaration(path) { /* ... */ }
}
};
93
@NicoloRibaudo
94
@NicoloRibaudo
class Counter extends Component {
render() {
return <p>It works!</p>;
}
}
95
@NicoloRibaudo
class Counter extends Component {
render() {
return <p>It works!</p>;
}
}
96
@NicoloRibaudo
function findMethod(node, name) {
}
findMethod(path.node, "render");
97
@NicoloRibaudo
function findMethod(node, name) {
const elem = node.body.body
}
findMethod(path.node, "render");
98
@NicoloRibaudo
function findMethod(node, name) {
const elem = node.body.body.find(el =>
t.isClassMethod(el)
);
}
findMethod(path.node, "render");
99
@NicoloRibaudo
function findMethod(node, name) {
const elem = node.body.body.find(el =>
t.isClassMethod(el, {
static: false, computed: false
})
);
}
findMethod(path.node, "render");
100
@NicoloRibaudo
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
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
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
visitor: {
ClassDeclaration(path) {
if (!isReactComponent(path.node)) return;
path.replaceWith(template.ast`
const ${path.node.id} = (props) => {
${findMethod(path.node, "render")};
};
`);
},
},
104
@NicoloRibaudo
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
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
class Counter extends Component {
render() {
return <p>Hi, {this.props.name}!</p>
}
}
107
@NicoloRibaudo
class Counter extends Component {
render() {
return <p>Hi, {this.props.name}!</p>
}
}
108
@NicoloRibaudo
class Counter extends Component {
render() {
return <p>Hi, {this.props.name}!</p>
}
}
109
@NicoloRibaudo
function rewritePropsUsage(path) {
}
110
@NicoloRibaudo
function rewritePropsUsage(path) {
path.traverse({
MemberExpression(path) {
},
});
}
111
@NicoloRibaudo
function rewritePropsUsage(path) {
path.traverse({
MemberExpression(path) {
const { node } = path;
if (
!node.computed
) {
}
},
});
}
112
@NicoloRibaudo
function rewritePropsUsage(path) {
path.traverse({
MemberExpression(path) {
const { node } = path;
if (
!node.computed &&
t.isThisExpression(node.object)
) {
}
},
});
}
113
@NicoloRibaudo
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
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
visitor: {
ClassDeclaration(path) {
if (!isReactComponent(path.node)) return;
path.replaceWith(template.ast`
const ${path.node.id} = (props) => {
${findMethod(path.node, "render")};
};
`);
},
},
116
@NicoloRibaudo
visitor: {
ClassDeclaration(path) {
if (!isReactComponent(path.node)) return;
rewritePropsUsage(path);
path.replaceWith(template.ast`
const ${path.node.id} = (props) => {
${findMethod(path.node, "render")};
};
`);
},
},
@NicoloRibaudo
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
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
class Counter extends Component {
state = { count: 0 };
render() {
return <p>
Total: {this.state.count}
</p>;
}
}
120
@NicoloRibaudo
class Counter extends Component {
state = { count: 0 };
render() {
return <p>
Total: {this.state.count}
</p>;
}
}
121
@NicoloRibaudo
class Counter extends Component {
state = { count: 0 };
render() {
return <p>
Total: {this.state.count}
</p>;
}
}
122
@NicoloRibaudo
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
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
@NicoloRibaudo
function findInitialState(node) {
const stateNode = findField(node, "state");
if (!stateNode) return;
}
126
@NicoloRibaudo
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
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
path.replaceWith(template.ast`
const ${componentId} = (props) => {
${findMethod(path, "render")};
};
`);
129
get : Identifier { … }
set : Identifier { … }
init : NumericLiteral { … }
...
@NicoloRibaudo
const state = findInitialState(path.node);
path.replaceWith(template.ast`
const ${componentId} = (props) => {
${findMethod(path, "render")};
};
`);
130
get : Identifier { … }
set : Identifier { … }
init : NumericLiteral { … }
...
@NicoloRibaudo
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
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
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
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
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
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
@NicoloRibaudo
< >
Does our codemod cover every case?
138
@NicoloRibaudo
Ignoring unsupported patterns
139
@NicoloRibaudo
Ignoring unsupported patterns
Complex state initialization
140
@NicoloRibaudo
Complex state initialization
141
class Counter extends Component {
state = { count: 0, label: "Likes" };
render() {
return <p>
{this.state.label}: {this.state.count}
</p>;
}
}
@NicoloRibaudo
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
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
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
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
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
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
Complex state initialization
148
class Counter extends Component {
state = getInisialState();
render() {
return <p>
{this.state.label}: {this.state.count}
</p>;
}
}
@NicoloRibaudo
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
Analyzing a pattern's frequency
150
@NicoloRibaudo
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
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
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
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
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
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
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
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
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
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
@NicoloRibaudo
162
@NicoloRibaudo
Back to our codemod ...
163
@NicoloRibaudo
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
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
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
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
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
function findClassProperties(node) {
const properties = new Map();
return properties;
}
169
@NicoloRibaudo
function findClassProperties(node) {
const properties = new Map();
for (const elem of node.body.body) {
}
return properties;
}
170
@NicoloRibaudo
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
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
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
visotor: {
ClassDeclaration(path) {
// ...
path.replaceWith(template.ast`
const ${componentId} = (props) => {
${useStateCalls};
${findMethod(path, "render")};
};
`);
}
}
174
@NicoloRibaudo
visotor: {
ClassDeclaration(path) {
// ...
path.replaceWith(template.ast`
const ${componentId} = (props) => {
${useStateCalls};
${findMethod(path, "render")};
};
`);
}
}
175
inc init : ArrowFunctionExpression {}
...
@NicoloRibaudo
// ...
path.replaceWith(template.ast`
const ${componentId} = (props) => {
${useStateCalls};
${findMethod(path, "render")};
};
`);
176
inc init : ArrowFunctionExpression {}
...
@NicoloRibaudo
const vars = findClassProperties(path.node);
// ...
path.replaceWith(template.ast`
const ${componentId} = (props) => {
${useStateCalls};
${findMethod(path, "render")};
};
`);
177
inc init : ArrowFunctionExpression {}
...
@NicoloRibaudo
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
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
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
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
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
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
function rewriteVarsUsage(path, props) {
}
184
@NicoloRibaudo
function rewriteVarsUsage(path, props) {
path.traverse({
MemberExpression(path) {
}
});
}
185
@NicoloRibaudo
function rewriteVarsUsage(path, props) {
path.traverse({
MemberExpression(path) {
const { node } = path;
if (!t.isThisExpression(node.object)) return;
if (node.computed) return;
}
});
}
186
@NicoloRibaudo
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
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
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
@NicoloRibaudo
So far we only implemented local transforms, modifying a self-contained AST node:
ClassDeclaration
191
@NicoloRibaudo
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
visotor: {
ClassDeclaration(path) {
/* ... */
if (
useStateCalls.length > 0
) {
findReactImport(path)
}
}
}
212
import { useState } from "react";
@NicoloRibaudo
visotor: {
ClassDeclaration(path) {
/* ... */
if (
useStateCalls.length > 0
) {
findReactImport(path)
}
}
}
213
import { useState } from "react";
@NicoloRibaudo
visotor: {
ClassDeclaration(path) {
/* ... */
if (
useStateCalls.length > 0
) {
findReactImport(path)
}
}
}
214
import { useState as useState } from "react";
@NicoloRibaudo
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
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
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
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
</Example>
219
@NicoloRibaudo
Complementary tools
220
@NicoloRibaudo
Complementary tools
Babel is great at doing two things:
221
@NicoloRibaudo
Complementary tools
Babel is great at doing two things:
222
@NicoloRibaudo
Complementary tools
Babel is great at doing two things:
223
@NicoloRibaudo
Complementary tools
Babel is great at doing two things:
Codemods also need:
224
@NicoloRibaudo
Complementary tools
Babel is great at doing two things:
Codemods also need:
225
@NicoloRibaudo
Complementary tools
Babel is great at doing two things:
Codemods also need:
226
@NicoloRibaudo
Complementary tools
Codemods also need:
227
@NicoloRibaudo
Complementary tools
Codemods also need:
228
@NicoloRibaudo
Complementary tools
Codemods also need:
229
recast
@NicoloRibaudo
Complementary tools
Codemods also need:
230
recast
JSCodeshift's CLI
@NicoloRibaudo
Complementary tools
Codemods also need:
231
recast
JSCodeshift's CLI
A manual for loop that iterates over the files
@NicoloRibaudo
Demo!
232
@NicoloRibaudo
Babel:�A refactoring tool
233
NICOLÒ RIBAUDO
Babel team
@nicolo-ribaudo
@NicoloRibaudo
nicolo.ribaudo@gmail.com
@NicoloRibaudo