Sharing JavaScript code without headache
Sometimes you have multiple apps that share a lot of common code.
For example, you have an SPA written in Vue and a Blog running on Eleventy, and you want to reuse your Tailwind design system between them.
The obvious solution is to create a package with common code and publish it as a private NPM package. This usually leads to frustration: publishing packages is not a very straightforward or failure-tolerant process, which is completely reasonable for public packages, but mostly a waste of time for private packages and small-to-medium teams.
I see this approach as overkill for most projects — there are much simpler alternatives:
Use Monorepo
The first one is to store code in a monorepo. Using the workspace feature, you will be able to install any package inside the monorepo.
.
├── .git
├── apps/
│ ├── spa/
│ │ ├── …
│ │ └── package.json
│ └── blog/
│ ├── …
│ └── package.json
├── packages/
│ └── some-package/
│ ├── …
│ └── package.json
├── pnpm-workspace.yaml
└── package.json
Then you add a special file called pnpm-workspace.yaml
that allows you to install folders as packages.
packages:
- 'packages/*'
- 'apps/*'
And then you can use them like this:
{
"name": "spa",
"dependencies": {
"some-package": "workspace:*"
}
}
Due to the fact that code is stored in one repo, you don’t need to think about versioning — code revisions are coupled via commits.
Installing Packages from GitHub URLs
If you don’t want to use a monorepo, you can install packages via package GitHub repo URLs.
This way is almost the same as working with standard npm packages, but with one twist — you use a git repo as a package. That means you have a very forgiving publishing flow where you can revert any of your actions.
pnpm i https://github.com/someorg/some-package#1.2.3
As you see — tags can be used to achieve reproducible builds.
"some-package": "github:someorg/some-package#1.2.3"
If you need to build some files — you can hook up a build script to the postinstall
script in package.json
.
{
"name": "some-package",
"main": "dist/main.css",
"scripts": {
"postinstall": "pnpm i; postcss -i src/main.css -o dist/main.css"
},
"dependencies": {
"postcss": "8.4.38" // PostCSS is declared in dependencies, not devDependencies because we need it to be accessible from the postinstall script
}
}
Just keep in mind that you must move build dependencies to dependencies
instead of devDependencies
. As this is a private package, this is not an issue, especially when you are using pnpm.
As your project grows, you might hit the ceiling with this approach and you might need to switch to standard private packages, but your project needs to grow to this point and before that, you can save a lot of time!
Monorepo, on the other hand, is future-proof. But you need to have a monorepo.
Usually, I use a monorepo on all code I write by myself. When I fork someone’s code, I install it via URL to keep the same project structure as the original repo.