Skip to content

Instantly share code, notes, and snippets.

@Blankeos
Last active January 17, 2025 03:46
Show Gist options
  • Select an option

  • Save Blankeos/cab78361dfbe89c902286f1e4ba5cbd5 to your computer and use it in GitHub Desktop.

Select an option

Save Blankeos/cab78361dfbe89c902286f1e4ba5cbd5 to your computer and use it in GitHub Desktop.
Publishing an NPM Package Guide

Publish an NPM Package

Login

npm login

Inside your Node Project

# Publish normally
npm publish

# Publish a scoped package in public (When you want to publish under your own name)
npm publish --access=public

# Check who's currently logged in
npm whoami

Automate

My recommended approach. Based on Matt Pockock as well. I use Bun btw, but still applicable for PNPM (if you're smart enough to replace the CI stuff, I put labels on them, should be easy).

Tools you need:

  • TSUP - for bundling
  • Changesets - for auto version bumps & changelogs
  • GitHub Workflows - for auto test and publish
  • an NPM account - where you'll publish

TSUp

# 1. Install on your npm package (If monorepo, install for every package)
bun add -D tsup
  1. Make the tsup.config.ts.
import { defineConfig } from 'tsup';

export default defineConfig({
  entry: ['src/index.tsx'],
  outDir: 'dist',
  format: ['cjs', 'esm'],
  dts: true,
  splitting: true,
  sourcemap: true,
  clean: true,
});
  1. Make your build and lint scripts
"lint": "tsc" # just a suggestion
"build": "tsup" # this emits `dist/index.js`, `dist/index.d.ts` and `dist/index.mjs` (sometimes)
  1. Set your package.json. (If monorepo, put in every package)
  "private": false,
  "type": "module",
  "main": "./dist/index.js",
  "module": "./dist/index.js", # can be ./dist/index.mjs if your build emits it.
  "types": "./dist/index.d.ts",
  "files": ["dist"]

Changesets

# 1. Install in your project (If monorepo, just install in root. Only needed once)
bun add -D @changesets/cli
bun add -D @changesets/changelog-github # not necessary but nice to have for changelogs

# 2. Initialize changesets
bun changeset init # or pnpm changeset init
# >> This should make .changeset/config.json and README.md

# 3. Go to .changeset/config.json and change `"access": "restricted",` to "public".

# 4. Make these package.json scripts (for CI-purposes)
# will run on `main.yml` on all branches
"ci": "bun lint && bun run build"

# will run via `publish.yml` on `main` branch (NOTE: Make sure the name is not 'publish' because it will run recursively.
# Because changeset publish runs `npm publish` which calls prepublish and publish again, yes weird. Don't do it.
"publish-ci": "bun lint && bun run build && changeset publish"

# 5. Make your changes...

# 6. COMMAND you'll regularly use:
bun changeset # You will do this whenever you want to make a release/version bump. Demonstrated below later.

GitHub Workflows

You only need two main.yml and publish.yml

# .github/workflows/main.yml - Purpose: Just to check/test your codebase.
# I got this from Matt Pockock
# https://www.youtube.com/watch?v=eh89VE3Mk5g
name: CI

on:
  push:
    branches:
      - '**'

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: oven-sh/setup-bun@v2 # replace with node + pnpm if you want.

      - run: bun install --frozen-lockfile # replace if you want.
      - run: bun run ci # replace if you want.
# .github/workflows/publish.yml - Purpose: Does two things, Create a "Release PR" or Publish to NPM
# I got this from Matt Pockock
# https://www.youtube.com/watch?v=eh89VE3Mk5g
name: Publish
on:
  push:
    branches:
      - 'main'

# Publish workflows don't happen at the same time.
concurrency: ${{ github.workflow }}-${{ github.ref }}

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: oven-sh/setup-bun@v2
        with:
          bun-version: latest
      - run: bun install --frozen-lockfile
      - run: bun run build

      - name: Create Release Pull Request or Publish
        id: changesets
        uses: changesets/action@v1
        with:
          publish: bun run publish-ci
        env:
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # TAKE NOTE, you have to generate this in NPM
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # You DON'T need to generate this, it's automatic by GitHub Workflows.

NPM (and some gotchas)

  1. Generate an Access Token
    • Go to NPM > Access Tokens > Generate New Token. (name it if you want). Make sure to copy it!
    • Then go to GitHub > Settings > Secrets and variables > Actions
    • Add a new repository secret and name it NPM_TOKEN. Make sure to paste your ACCESS TOKEN here.
  2. In NPM, make sure to turn off 2FA for writes. So publish works in CI without --otp.
    • Go to NPM > Account > Modify 2FA > Additional Options
      • Require two-factor authentication for write actions
  3. Last gotchas with GitHub, make sure to do this:
    • In GitHub, go to GitHub > Settings > Actions > General and set:
      • Workflow Permissions: Read and write permissions
      • Allow GitHub Actions to create and approve pull requests

Cool but how does this work?

The instructions above just taught you how to setup the automations, but what's the routine thing you'll do as a dev and how do your automations interact with you?

How it flows:

  • Make your changes (as usual).
  • If you want these changes set for a release, just run bun changeset.
    • Just follow the instructions of choosing major, minor, patch
    • Then write a summary (This will be used in the changelog).
    • After this, it emits files in .changelog/<some-jibberish>.md
  • If you're good, just commit to main and push. (Or make a PR to main).
  • After that publish.yml will trigger, it will know that you have <some-jibberish>.md and automatically creates a Release PR.
    • This PR removes <some-jibberish>.md and bumps the package version.
  • If you want to Publish it to NPM, find the PR, and press Merge.
  • After it merges to main, publish.yml will run again, but this time it notices that there are no pending changesets, hence it will just publish automatically!

Hope that helps!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment