This article is more or less an advertisement for an npm package that I have just released: basetag. I want to share how the package came to be and why I believe it’s pretty awesome.
A Bit of Backstory
Node.js projects — like all software development projects — can get somewhat complex over time: developers often refactor functionality into separate modules, subdirectories, and helper classes; in less stressful times tech debt can be paid off.
As a system evolves, its complexity increases unless work is done to maintain or reduce it.
— Lehman’s Laws of Software Evolution
Also nowadays, the monorepo has become increasingly popular again. This shows that projects and their structures can become very large in scope. Different programming languages have different approaches to working with this. Most modern programming languages use namespaces and modules/packages. Some example are listed below.
-
Swift has modules and implicit namespaces (e.g.
import Foundation
) -
Python has modules (e.g.
import pandas as pd
) -
Java has packages (e.g.
import java.util.Date
) - Ruby, Rust, C++, and many more have similar concepts somewhere. Heck, Linux itself has a namespaces(7) API!
Yet, in Node.js we can only import local modules via relative path…
Node.js Imports
If you’ve used Node.js, you know this and have seen many statements like the following one.
const myModule = require('./MyModule')
Now that doesn’t seem too bad… But let’s consider a more complex project. Most of the time we will be importing modules that aren’t far away. Yet, it can happen that we have modules that are loosely coupled and far away (in terms of files). Please consider the following directory structure (although it might be a fabricated example and maybe even indicate some code smells).
example/
├── its/
│ ├── …
│ └── baseballs/
│ ├── …
│ └── all/
│ ├── …
│ └── the/
│ ├── …
│ └── way/
│ ├── …
│ └── down.js
├── somewhere/
│ ├── …
│ └── deep/
│ ├── …
│ └── and/
│ ├── …
│ └── random.js
├── …
└── index.js
You get the picture — we have a bunch of directories with a bunch of files. Now say we want to reference example/somewhere/deep/and/random.js
from example/its/baseballs/all/the/way/down.js
. In other languages we could probably import somewhere.deep.and.random as rand
, but in Node.js this gets pretty messy and would look like the following import statement.
const randomRelative = require('../../../../../somewhere/deep/and/random')
This has always frustrated me considerably and I started doing some research. It turns out that there are a lot of tweets and blog posts that complain about this problem. But there are also some projects that try to tackle the problem.
One approach (the npm package app-root-path
tries to find the root path of a project and lets you import relative to that path. They even include a nifty .require
method that you can reuse. This is already pretty cool!
const projectRoot = require('app-root-path');
const random = require(projectRoot + '/somewhere/deep/and/random.js');
// OR using .require
const requireLocal = require('app-root-path').require
const random = requireLocal('somewhere/deep/and/random');
You could even store the requireLocal
method into your globals 😱 in your entry file and it would be available in all other executed files. The package is great but I wanted to find something that feels even more native.
I continued my search and came across some blog posts that proposed symlinks to reference to the base path of a project.
That’s how the idea for basetag was born.
The basetag package consists only of a postinstall
script that adds a symlink $
inside node_modules
. That symlink points to the base path of your project. Node.js now basically thinks that there is a $
module installed and you can require submodules of $
(which in turn just point to your project files).
const randomRelative = require('../../../../../somewhere/deep/and/random')
// Using 'basetag' becomes...
const randomBasetag = require('$/somewhere/deep/and/random')
All you need to do is install basetag (e.g. via npm i -S basetag
) and you can start using the $/…
prefix in require statements.
- The prefixed require is very readable, simple, and it’s pretty obvious what’s going on.
- The prefixed require is still mixable with traditional relative requires. Since Node.js is literally using the same files (just routing differently), the imports are cached correctly.
- The solution works with Node.js versions ≥ v4.x.
- The package is super simple and has zero dependencies.
Well, that was my path to creating the tiny package basetag. Feel free to check it out and use it in your projects. Be aware that this package is stable but still very young — make sure to have all your files safe in version-control before using it. Due to the simple nature of the project there will probably not be a lot of updates to be expected…
Top comments (4)
The title says 'import' but the code uses require. I've yet to find a nice method for this that works with es6.
Good point, I’ll try to test this with import statements and rewrite if it doesn’t work
Btw, you can use aliasing. Ive mentioned it on one of my posts, the guide to es6 one. I don't remember it sorry lol. It lets you use ~ as project root (or some specific directory of your choice) in your import statements.
It is a great package but it breaks every time you install another npm package, I had to uninstall it, install the new npm package and then re-install it so my app could work again.
If there is a fix, do let me know because I am about to uninstall permanently and look at an alternative