Many of our React projects are based on create-react-app (CRA), to make it easy to maintain and upgrade dependencies over time. This is a great starter-kit, but sometimes we want to do things that are not possible to do in the default version of CRA, which gives us two options:
While ejecting is the easiest thing to do, it removes the benefits we get out of CRA when it comes to upgrading core dependencies over time. We are also forced to replicate our changes on every single app if we don't have a shared react-scripts.
Making our own version of create-react-app is a way to give us the flexibility to customize the configuration, while still making it easy to share it across different projects. We just need a way to make sure that our version of CRA easily can be upgraded as the original version moves forward.
Usually when you fork a package you create a replica of the whole repository, make some changes and use your own version instead of the one that is published to npm. In the case of create-react-app (and many other tools these days), they are using a monorepo. Simply put, this means that several packages are kept in one repository, making it easier to develop packages that have dependencies on each other.
When setting up a project using create-react-app, you'll find out that the only dependency we actually use after initializing the app, is the package react-scripts. This means that we are not really interested in forking react-dev-utils, babel-preset-react-app or any of the other packages that are also in the monorepo.
Many of our projects are also using a monorepo structure, meaning that ideally we want to have react-scripts living alongside our project. So how in the world do we fork parts of a monorepo and insert them into our own monorepo, while also maintaining the possibility to update the package when there's a new version, without having to revert all of our changes and start over?
Luckily, there's a good, but pretty unknown, answer to that.
This amazing feature should not be confused with git submodule, which is a whole another story. git subtree lets you import parts of a git repository into another repository, while keeping the commit history, meaning you can update your local version when the remote has changed.
This might sound like magic, but it's not. Lets go through it step by step.
$ git clone https://github.com/facebook/create-react-app.git
$ git checkout v2.1.5
$ git subtree split -P packages/react-scripts -b react-scripts
This will create a specific branch containing only what's inside the path given (by the -P flag), in this case only the package we want - react-scripts.
$ git remote add react-scripts-remote ../relative/path/to/source/repo
$ git subtree add -P packages/react-scripts react-scripts-remote react-scripts-v2.1.5 --squash
--squash means that the whole commit history will be squashed into a single commit when we add the package. This is usually what you want to do when forking an external package like this. If you want, you can omit
--squash but your commit log will then contain the full history for all the files in the repo you just forked out of. This might or might not be desirable.
Now you should have your own fork of react-scripts in your repository, free to do whatever modifications you want!
An important feature of git subtree is that you're able to update your fork when the remote has changed, such as when a new version of the package has been released.
$ git subtree pull-P packages/react-scripts react-scripts-remote react-scripts-v2.1.6 --squash
After you've fixed potential merge conflicts, you should have an updated version of your forked package. Voilà!
With these commands I've been able to handle all cases I've stumbled upon when working with git subtrees. There's not a lot of good resources on how it works, but if you want to learn more about this, checkout the man page.
Written by Alfred Ringstad