Photo by Maksym Kaharlytskyi on Unsplash
After many years of software development, I finally published my own NPM package. In fact, I published two. I was working on my checksync
tool when I realised that I needed the package that this blog introduces. More on checksync
in the next entry.
https://www.npmjs.com/package/ancesdir
🤔 What is root? Where is root?
Quite often, when working on some projects at Khan Academy, we need to know the root directory of the project. This enables us to write tools, linters, and tests that use root-relative paths, which in turn can make it much easier to refactor code. However, determining the root path of a project is not necessarily simple.
First, there is working out what identifies the root of a project. Is it the node_modules
directory? The package.json
file? The existence of .git
folder? It may seem obvious to use one of these, but all these things have something in common; they don't necessarily exist. We can configure our package manager to have package.json
and node_modules
in non-standard places and we might change our source control, or not even run our code from within a clone of our repository. Determining the root folder by relying on any of these things as a marker is potentially not going to work.
Second, the code to walk the directory structure to find the given "marker" file or directory is not trivial. Sharing a common implementation within your project means everything that needs it, needs to locate it; in JavaScript, that means a relative path, at which point, you may as well just use a relative path to the known root directory and skip the shared approach all together. Yet, if you don't share a common implementation from a single location, then the code has to be duplicated everywhere you need it. I don't know about you, but that feels wrong.
💁🏻♂️ Solution: ancesdir
The issue of sharing a common implementation is easiest to solve. If that common implementation is installed as an NPM package, we don't need to include it via a relative path; we can just import it by its package name. There are packages out there that do this, but the ones I found all assumed some level of default setup, failing to acknowledge that this may change. In turn, they did not support a monorepo setup where there could be multiple sub-projects. How could one find the root folder of the monorepo from within a sub-project if all we used to identify the root folder were package.json
? What if we wanted to sometimes get the root of the sub-project and sometimes the root of the monorepo?
I needed a way to identify a specific ancestor directory based on a known marker file or directory that would work even with non-standard setups. At Khan Academy, we have a marker file at the root of the project that is there solely to identify its parent directory as the project root. This file is agnostic of tech stack; it's just an empty file. It is solely there to say "this directory is the root directory". No tooling changes are going to render this mechanism broken unexpectedly unless they happen to use the same filename, which is unlikely. This way, we can find the repository root easily by locating that file. I wanted a package that could work just as easily with this custom marker file as it could with package.json
.
I created ancesdir
to fulfill these requirements1.
yarn add ancesdir
The API is simple. In the default case, all you need to do is:
import ancesdir from "ancesdir"; console.log(`ancesdir's root directory is ${ancesdir()}`);
If you have a standard setup, with a package.json
file, you will get the ancestor directory of the ancesdir
package that contains that package.json
file.
However, if you want the ancestor directory of the current file or a different path, you might use ancesdir
like this:
import ancesdir from "ancesdir"; console.log(`This file's root directory is ${ancesdir(__dirname)}`);
In this example, we have given ancesdir
a path from which to being its search. Of course, that still only works if there is an ancestor directory that contains a package.json
file. What if that's not what you want?
For the more complex scenarios, like monorepos, for example, you can use ancesdir
with a marker name, like this:
import ancesdir from "ancesdir"; console.log(`The monorepo root directory is ${ancesdir(__dirname, ".my_unique_root_marker_file")}`);
ancesdir
will then give you the directory you seek (or null if it cannot be found). Not only that, but repeated requests will work faster as the results are cached as the directory tree is traversed.
Conclusion
If you find yourself needing a utility like this, checkout ancesdir
. I hope y'all find it useful and I would love to hear if you do. You can checkout the source on GitHub.
- The name is a play on the word "ancestor", while also attempting indicate that it has something to do with directories. I know, clever, right? [↩]