Using GitHub Releases for patching Monorepo npm package
Tags: npm, github
Published: 2021-07-29 12:40:12

When you find a bug in a npm package, you'll send a Pull Request (PR) to the original package. It would be great if your PR are merged quickly and released in the new version in npm registry. However, sometime it takes time but you want to use your bug fix right away. How can we do that?

It is simple if the package is the root of GitHub repository i.e. not Monorepo because `package.json` supports [GitHub URL in the dependencies](https://docs.npmjs.com/cli/v7/configuring-npm/package-json#github-urls). Since you have already sumitted the PR, you should have your own fork with the bug fix. Therefore, you can simply specify your branch in your `package.json` and `npm` command handles the rest including running `prepare`.

Things are not so easy if the package is managed by a repository having multiple packages in it a.k.a. Monorepo. Unfortunately, there is no universal support by `package.json` because each monorepo is managed differently. Therefore, you need to build the package on your forked branch and use it from your application.

There are a bunch of solutions here, but I'd like to introduce a simple solution using GitHub Releases. In this solution, you just need a few manual steps:

1. Build the monorepo e.g. `lerna run build`
2. Create `.tgz` package for your patched version e.g. `cd packages/package/dis && npm pack`
3. Publish GitHub Releases i.e. `gh release create ... package.tgz`
4. Use the artifact URL in your application's dependencies

You need to run 1-3 whenever you update the branch, and probably need to reinstall packages in your application. You can automate these steps by GitHub Actions but you need to add `action.yaml` to your branch which is unrelated to your PR. Thus, I think this solution will work better with a repository getting less frequent update in upstream.

## Steps

Let me give you a concrete example I did. I needed [this fix](https://github.com/tw-in-js/use-twind-with/pull/21) in my application right away. [The repository](https://github.com/tw-in-js/use-twind-with) is monorepo and managed by `lerna`. I've already checked the forked repository out and had a branch named `fix-classname` on my laptop.

### Build the monorepo

Simply built the monorepo:

```zsh
➜  use-twind-with git:(fix-classname) npx lerna run build
```

This created `dist` directory under the package and it contains JavaScript files transpiled from TypeScript:

```zsh
➜  use-twind-with git:(fix-classname) cd packages/preact
➜  preact git:(fix-classname) ls ./dist
CHANGELOG.md         package.json         preact.d.ts          preact.esnext.js.map preact.mjs
LICENSE              preact.cjs           preact.d.ts.map      preact.js            preact.umd.js
README.md            preact.cjs.map       preact.esnext.js     preact.js.map        preact.umd.js.map
```

### Create `.tgz` package

I just needed `dist` directory only, so I ran `npm pack` there:

```zsh
➜  preact git:(fix-classname) cd dist
➜  dist git:(fix-classname) npm pack
npm notice
npm notice 📦  @twind/preact@1.0.5
npm notice === Tarball Contents ===
npm notice 1.4kB CHANGELOG.md
npm notice 1.1kB LICENSE
npm notice 2.5kB README.md
npm notice 1.3kB package.json
npm notice 3.9kB preact.cjs
npm notice 3.7kB preact.cjs.map
npm notice 751B  preact.d.ts
npm notice 92B   preact.d.ts.map
npm notice 1.6kB preact.esnext.js
npm notice 3.4kB preact.esnext.js.map
npm notice 1.7kB preact.js
npm notice 3.4kB preact.js.map
npm notice 108B  preact.mjs
npm notice 1.5kB preact.umd.js
npm notice 3.3kB preact.umd.js.map
npm notice === Tarball Details ===
npm notice name:          @twind/preact
npm notice version:       1.0.5
npm notice filename:      @twind/preact-1.0.5.tgz
npm notice package size:  6.9 kB
npm notice unpacked size: 29.8 kB
npm notice shasum:        b258372501fb0c0f392274e237f230575c56342f
npm notice integrity:     sha512-y+zDKMLhQNSUu[...]UpgzvPXvPto7w==
npm notice total files:   15
npm notice
twind-preact-1.0.5.tgz
```

Now, I got `twind-preaxt-1.0.5.tgz` file.

Note: If you run `npm pack` in the package root, the `.tgz` file points TypeScript file as `main` and won't work with `npm install`.

### Publish GitHub Releases

To release, I needed a Git tag. I used a convention like `<original-tag>-<branch-name>`. In this case, original tag was `@twind/preact@1.0.5` and the branch name was `fix-classname`. I needed to create and push the tag first. Then, published the tag with the packed `.tgz` file:

```zsh
➜  dist git:(fix-classname) git tag @twind/preact@1.0.5-fix-classname
➜  dist git:(fix-classname) git push --tags
➜  dist git:(fix-classname) gh release create \
    --target fix-classname \
    --title @twind/preact@1.0.5-fix-classname \
    --notes '' \
    @twind/preact@1.0.5-fix-classname twind-preact-1.0.5.tgz
```

This is the actual release page: https://github.com/riywo/use-twind-with/releases/tag/%40twind%2Fpreact%401.0.5-fix-classname

Note: `gh` command can be installed by https://github.com/cli/cli#installation

### Use the artifact URL

Now, I got the `.tgz` public URL `https://github.com/riywo/use-twind-with/releases/download/%40twind%2Fpreact%401.0.5-fix-classname/twind-preact-1.0.5.tgz`. The last step was simply adding this to my application's `package.json`:

```json
  "dependencies": {
    "@twind/preact": "https://github.com/riywo/use-twind-with/releases/download/%40twind%2Fpreact%401.0.5-fix-classname/twind-preact-1.0.5.tgz",
    ...
  },
```

Then, ran `npm install`. That's it! I compared the files under `node_modules/@twind/preact` and they were at the patched version:

```zsh
➜  app git:(main) ✗ ls node_modules/@twind/preact
CHANGELOG.md         package.json         preact.d.ts          preact.esnext.js.map preact.mjs
LICENSE              preact.cjs           preact.d.ts.map      preact.js            preact.umd.js
README.md            preact.cjs.map       preact.esnext.js     preact.js.map        preact.umd.js.map
```



## Alternatives

You can do the similar thing without the public artifact, for example with Git Submodule/Subtree + custom npm scripts. This would be great because you don't have to manage GitHub Releases artifacts. However, the build process of your application might become complicated and take more time.

## Conclusion

I explained my solution to use a forked branch of monorepo packages via GitHub Releases. It requires a few manual steps but is relatively simple and easy to maintain.