How to update a few years old outdated project in Node and React?
Tags: webdev, javascript, tutorial, react
Published: 2023-08-24 08:40:06

Navigating old Node.js dependencies can be confusing. In this article, I'll share how I decoded this challenge in a few-year-old repository using a custom tool—and how you too can benefit from my experience.

> tl;dr
> Use the tool [dependency-time-machine](https://www.npmjs.com/package/dependency-time-machine) to quickly update dependencies one by one in chronological order. Most dependencies are compatible with other packages from a similar or pastime.
> This tool is intended to simulate the typical updating workflow as it was done regularly.
> ```
> npx dependency-time-machine --update
> ```

### Outdated codebase

![GTA SA](https://media.tenor.com/cJRcMyUAiMcAAAAd/ah-shit-here-we-go-again-ah-shit.gif)

Recently I joined a new team with a great product but a highly outdated codebase. So my first task was to update dependencies and made changes so we can migrate from Node 16 to Node 18 and finally to Node 20. Some parts of the code were still relying on Node 14. The initial thought was to use ``ncu`` or ``yarn upgrade-interactive`` and update all patch versions since they shouldn't break anything if package developers were following the semver. But they didn't.

### Dependency void
Many dependencies were not only "old" - this is not the problem, but they were incompatible with new Node versions and other dependencies which features we looked for. At the moment of doing this process, Node 16 becomes deprecated and Node 18 is going into maintenance. Having such an old codebase makes it hard to further develop and with backend part makes it also insecure and vulnerable.

Dependency management is not only about blindly updating, but also about knowing when it's beneficial to use or drop package and having common sense about long term tech debt.
Sometimes, newer versions of a dependency can introduce breaking changes, reduced performance, or other issues. Before updating, developers should check changelogs, understand the implications of the update, and, ideally, test the update in a controlled environment before deploying it.

Over time, some dependencies might become redundant, or better alternatives might emerge. A good practice is to periodically review the project's dependencies to determine if they are still aligned with the project's needs. Additionally, if a package is no longer maintained or has known vulnerabilities, it might be wise to seek alternatives or drop it altogether.

Over-reliance on multiple dependencies can lead to technical debt. If not managed properly, this debt can accumulate, making future changes or updates cumbersome and risky. It's essential to strike a balance between leveraging existing solutions (dependencies) and the potential long-term costs associated with them.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vi4mfd0a86vf44g5iaxs.png)

Going back to the case. Thankfully the team wrote plenty of good tests, both unit tests, some integration and a lot of end-to-end so after each update it was possible to check if something broke. At this point, I realized I have to go one-by-one dependency and look at which dependency will be compatible with others and SDK. 

Some packages had phantom dependencies. They are indirect dependencies (but also not peerDependencies) and it is expected to have them installed and be available in certain directories. npm since v3 and yarn classic install dependencies in the flat structure of node_modules with nesting only sub-dependencies so the issue with phantom dependencies is hidden until changes in the package structure. pnpm - another package manager installs dependencies in a hidden directory and uses symlinks to make them available at node_modules levels. Also, it hides all sub-dependencies which brokes phantom dependencies early, leaving clean node_modules (IMO, that's good).

![npm v2 vs v3](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3ob76w8dnlzbf8m3uvdy.png)

Other problems were changes in Node SDK from 16 to 18. In the meantime, Node 17 introduced changes in the open SSL provider which brokes older webpack and DNS resolve mechanism from "by-default-ipv4" to "by-default-ipv6" which brokes usage of some backend and testing libraries.

### dependency-time-machine

To maintain all this headache I wrote in my spare time a library called [dependency-time-machine](https://www.npmjs.com/package/dependency-time-machine) which retrieves information about every package listed in package.json and its version. Then it creates a single timeline which is used to determine which library is the most behind and should be updated next.

I found libraries written at certain moments are compatible with other libraries at similar or previous timespan. So, for instance, library ``a@2.5.1`` that came when library ``b@1.3.0`` was already available makes it higher chance compatible than with the same library but with a higher version e.g. ``b@1.3.2`` that was released a year later than ``a@2.5.1``.

```
npx dependency-time-machine --timeline
```

Moreover, this replicates the natural process of updating dependencies. When you do this regularly you match existing libs with other existing libraries. So, the main problem that  ``dependency-time-machine`` tries to solve is to reconstruct the process of natural updating.

And because it may be a lot of work, like in my case where I had to take care of over 200 packages (!) so to ease this process I put into tool ``auto mode`` which can find the next dependency to update before another dependency was released, install it and run tests. If tests pass, it will do the same over and over. If the tests fail it will stop so you can look closer and fix either codebase or test.

```
npx dependency-time-machine --update --install --auto --install-script "yarn install" --test-script "yarn test"
```

Also, if you don't want to rely on auto-mode you can print the timeline in JSON and make decisions manually of updating dependencies. Other options allow you to exclude dependencies you don't want to update for some reason.

Going back to the story. It still took some time but at least I was more certain about the process and had the linear plan of work. After all, I wrote the document to describe the process so other team members know what's going on with the repository and discussed the process. Deployment went smoothly with only few minor issues in the meantime because most of the problems were spotted early thanks to willing help from team members.

Dependency management can be painful, but done regularly and with patience makes our future work easier. I hope someone finds this article and tool useful :)