Refactoring Large JavaScript Codebases
Michele Riva
Michele Riva
Senior Software Architect @NearForm
Google Developer Expert
Microsoft MVP
MicheleRivaCode
Real-World Next.js
Build scalable, high performances and modern web applications using Next.js, the React framework for production
MicheleRivaCode
Refactoring is hard
MicheleRivaCode
MicheleRivaCode
node -p "Promise.reject()"
Node 14.x
[DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated.
In the future, promise rejections that are not handled will terminate
the Node.js process with a non-zero exit code.
MicheleRivaCode
node -p "Promise.reject()"
Node 16.x
[UnhandledPromiseRejection: This error originated either by throwing inside of
an async function without a catch block,
or by rejecting a promise which was not handled with .catch().
The promise rejected with the reason "undefined".]
MicheleRivaCode
const user = {
data: {
name: {
first: "Michele",
last: "Riva"
}
}
}
const middleName = user.data
&& user.data.name
&& user.data.name.middle
|| "No middle name";
console.log(middleName);
Node 12.x
No middle name
MicheleRivaCode
const user = {
data: {
name: {
first: "Michele",
last: "Riva"
}
}
}
const middleName = user.data?.name?.middle ?? "No middle name";
console.log(middleName);
Node 12.x
SyntaxError: Unexpected token '.'
MicheleRivaCode
const user = {
data: {
name: {
first: "Michele",
last: "Riva"
}
}
}
const middleName = user.data?.name?.middle ?? "No middle name";
console.log(middleName);
Node >14.x
"No middle name"
MicheleRivaCode
import get from 'lodash/get';
const user = {
data: {
name: {
first: "Michele",
last: "Riva"
}
}
}
const middleName = get(user, "data.name.middle", "No middle name");
console.log(middleName);
"No middle name"
MicheleRivaCode
import reverse from 'lodash/reverse';
import isString from 'lodash/isString';
import isFunction from 'lodash/isFunction';
import isNull from 'lodash/isNull';
import split from 'lodash/split';
import filter from 'lodash/filter';
import map from 'lodash/map';
import keys from 'lodash/keys';
// and so on...
MicheleRivaCode
import reverse from 'lodash/reverse';
// Array.reverse()
import isString from 'lodash/isString';
// typeof
import isFunction from 'lodash/isFunction';
// typeof
import isNull from 'lodash/isNull';
// typeof
import split from 'lodash/split';
// Array.split()
import filter from 'lodash/filter';
// Array.filter
import map from 'lodash/map';
// Array.map
import keys from 'lodash/keys';
// Object.keys
// and so on...
MicheleRivaCode
What if we have thousands of files to refactor?
MicheleRivaCode
MicheleRivaCode
Divide and conquer
Approach #1
MicheleRivaCode
1. Divide the codebase with codeowners
MicheleRivaCode
* @org/platform-team
# Design System
/packages/design-system/** @org/fe-team
# Machine Learning models
/packages/machine-learning/** @org/ml-team @org/data-science-team
# QA
/tests/cypress/** @JohnDoe @JaneDoe
# Libraries, utilities
/packages/lib/** @MicheleRiva @org/platform-team @org/arch-team
MicheleRivaCode
1. Divide the codebase with codeowners
2. Create/install linting rules (warning stage)
MicheleRivaCode
1. Divide the codebase with codeowners
2. Create/install linting rules (warning stage)
3. Create/install linting rules (error stage)
MicheleRivaCode
1. Divide the codebase with codeowners
2. Create/install linting rules (warning stage)
3. Create/install linting rules (error stage)
4. Assign the remaining parts to codeowners
MicheleRivaCode
1. Divide the codebase with codeowners
2. Create/install linting rules (warning stage)
3. Create/install linting rules (error stage)
4. Assign the remaining parts to codeowners
5. Run linter on the entire project an fix remaining parts
MicheleRivaCode
Pros
Cons
- Ensures correctness
- Incremental adoption
- Everyone is responsible
- Future-proof
MicheleRivaCode
Pros
Cons
- Ensures correctness
- Incremental adoption
- Everyone is responsible
- Future-proof
- Requires time
- Great team effort
- Needs a coordinator
MicheleRivaCode
Codemod
Approach #2
codemod -m -d /home/jrosenstein/www --extensions php,html \
'<font *color="?(.*?)"?>(.*?)</font>' \
'<span style="color: \1;">\2</span>'
"Codemod is a tool/library to assist you with large-scale codebase refactors that can be partially automated but still require human oversight and occasional intervention."
MicheleRivaCode
JSCodeShift
MicheleRivaCode
MicheleRivaCode
Parsing
Transformation
Codegen
MicheleRivaCode
Parsing
Transformation
Codegen
MicheleRivaCode
Parsing
Step 1
Tokenization
var foo = 10
var
foo
=
10
input
tokens
MicheleRivaCode
Parsing
Step 2
Syntactic Analysis
var foo = 10
=
/ \
/ \
var 10
|
foo
input
parse tree
(aka concrete syntax tree)
MicheleRivaCode
Parsing
Step 2
Syntactic Analysis
var foo = 10
variableDeclaration
|
|
vairiableDeclarator
/ \
/ \
Identifier NumericLiteral
input
abstract syntax tree (AST)
MicheleRivaCode
MicheleRivaCode
{
"body":[
{
"type":"VariableDeclaration",
"declarations":[
{
"type":"VariableDeclarator",
"id":{
"type":"Identifier",
"name":"foo",
"loc":{
"identifierName":"foo"
}
},
"init":{
"type":"NumericLiteral",
"extra":{
"rawValue":10,
"raw":"10"
},
"value":10
}
}
],
"kind":"var"
}
]
}
MicheleRivaCode
MicheleRivaCode
export const parser = "babel";
export default function transformer(file, api) {
const j = api.jscodeshift;
return j(file.source)
.find(j.Identifier)
.forEach((path) => {
if (j(path).get().value.name.includes("oldName")) {
j(path).replaceWith(
j.identifier(path.node.name.replace("oldName", "newName"))
);
}
})
.toSource();
}
Transforming
MicheleRivaCode
const oldNameFactory = () => {/* */};
const newNameFactory = () => {/* */};
input
output
Codegen
MicheleRivaCode
import MyHeader from 'components/MyHeader';
export function MyApp(props) {
return (
<div>
<MyHeader {...props.headerProps} />
<p> Hello, {props.name}!</p>
</div>
)
}
export function MyApp(props) {
return (
<div>
<p> Hello, {props.name}!</p>
</div>
)
}
input
output
Desired output
MicheleRivaCode
export const parser = 'babel'
export default function transformer(file, api) {
const j = api.jscodeshift;
const withoutElement = j(file.source)
.find(j.JSXElement)
.forEach(function (path) {
if (path.value.openingElement.name.name === "MyHeader") {
path.prune();
}
})
.toSource();
const withoutImport = j(withoutElement)
.find(j.ImportDefaultSpecifier)
.forEach(function (path) {
if (path.value.local.name === "MyHeader") {
path.parentPath.parentPath.prune();
}
})
.toSource();
return withoutImport;
};
MicheleRivaCode
export default function(context) {
return {
TemplateLiteral(node) {
context.report({
node,
message: 'Do not use template literals',
fix(fixer) {
if (node.expressions.length) {
// Can't auto-fix template literal with expressions
return;
}
return [
fixer.replaceTextRange([node.start, node.start + 1], '"'),
fixer.replaceTextRange([node.end - 1, node.end], '"'),
];
},
});
}
};
};
ESLint example
MicheleRivaCode
MicheleRivaCode
Pros
Cons
- Quick action
- Requires less team effort
- Won't affect other teams directly
MicheleRivaCode
Pros
Cons
- Quick action
- Requires less team effort
- Won't affect other teams directly
- Requires good CS skills
- Tests are absolutely needed
- QA is a must
MicheleRivaCode
MicheleRivaCode
Personal opinion
MicheleRivaCode
MicheleRivaCode
@MicheleRiva
@MicheleRivaCode
/in/MicheleRiva95
www.micheleriva.dev
Refactoring Large JavaScript Codebases
By Michele Riva
Refactoring Large JavaScript Codebases
- 494