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

  • 405