About Skills Work Journey Blog Contact
Open Source · 4 min read · March 2026

What I Learned Publishing My First NPM Package

Publishing stalejs to NPM was more involved than I expected. Writing the library took a weekend. Getting the package.json, build config, and release process right took another two days. Here's everything I wish I'd known upfront.

Bundling: ESM and CJS Both Matter

The JavaScript ecosystem is split. Some environments use CommonJS (require()), others use ES Modules (import). If you only ship one, you'll break someone's setup.

I use Rollup to generate both from the same source:

// rollup.config.js
export default [
  {
    input: 'src/index.js',
    output: { file: 'dist/index.cjs.js', format: 'cjs', exports: 'named' }
  },
  {
    input: 'src/index.js',
    output: { file: 'dist/index.esm.js', format: 'esm' }
  }
]

Then in package.json, point to both:

{
  "main": "dist/index.cjs.js",
  "module": "dist/index.esm.js",
  "exports": {
    ".": {
      "import": "./dist/index.esm.js",
      "require": "./dist/index.cjs.js"
    }
  }
}

The exports field is the modern way — bundlers like webpack and Vite use it preferentially. The main and module fields are fallbacks for older tooling.

TypeScript Types Without Writing TypeScript

stalejs is written in plain JavaScript, but TypeScript users expect types. You can ship a .d.ts file manually without migrating your whole codebase to TypeScript:

// dist/index.d.ts
export interface StaleOptions {
  ttl: string | number
  refetch: () => Promise<any>
  update: (el: Element, data: any) => void
  pauseWhenHidden?: boolean
  pauseWhenOffscreen?: boolean
}

export function stale(
  selector: string | Element,
  options: StaleOptions
): { destroy: () => void }

Then add to package.json:

"types": "dist/index.d.ts"

TypeScript consumers now get full autocomplete and type checking. No TypeScript compiler needed in your project.

The sideEffects Field

This one is easy to miss but important. Add this to package.json:

"sideEffects": false

This tells bundlers like webpack that your module has no side effects — meaning if a consumer imports your package but doesn't use a particular export, the bundler can safely drop it (tree-shake). Without this flag, webpack will include everything even if only one function is used.

Only set this if your package genuinely has no side effects (no global mutations, no CSS imports, no polyfills that run on import). stalejs qualifies — it only does things when you call stale().

Semantic Versioning From Day One

I know it feels over-engineered for a small library. Do it anyway.

The convention: MAJOR.MINOR.PATCH

  • PATCH (1.0.1) — bug fix, no API change
  • MINOR (1.1.0) — new feature, backwards compatible
  • MAJOR (2.0.0) — breaking change

Start at 0.1.0 if it's not stable yet. The leading zero signals that the API may change. Once you're confident in the API, bump to 1.0.0.

I use npm version patch|minor|major — it bumps the version in package.json, creates a git commit, and tags it automatically.

The README Is the Product

Nobody reads source code before deciding whether to install a package. They read the README. The README needs to answer five questions in under 30 seconds:

  1. What does this do?
  2. How do I install it?
  3. What does the simplest usage look like?
  4. What are all the options?
  5. What's the licence?

Start with the install command and a working code example. Everything else comes after.

The Publish Command

Once your build is ready:

npm run build       # generate dist/
npm version patch   # bump version + tag
npm publish         # ship it

Add a files field to package.json to control what gets published — you don't want to ship your src/, tests, or config files:

"files": ["dist", "README.md"]
stalejs is on NPM at npmjs.com/package/stalejs and GitHub at github.com/kptaan13/stalejs. Stars welcome.
Share

More Writing