DEV Community

Msotafa Mohamed
Msotafa Mohamed

Posted on

Three bugs my npm packages shipped, and the checks that catch them now

I maintain four small React libraries: react-loadly, accessly, compressly and tonely. This month I found three bugs in them that my own test suites never caught. They only showed up in other people's projects.

Here they are, with the check I now run before every publish.

1. The missing "use client"

The symptom. Someone imports <PermissionProvider> in a Next.js App Router layout and gets:

TypeError: createContext is not a function
Enter fullscreen mode Exit fullscreen mode

The cause. In the App Router, every file is a Server Component unless it says otherwise. My React entry used createContext and hooks, but nothing in the published file marked it as client code. So Next.js ran it on the server, where those APIs don't exist.

The fix. The built file has to start with the directive:

"use client";
Enter fullscreen mode Exit fullscreen mode

Putting it in your source isn't enough. Bundlers often merge modules and drop directives, and minifiers can strip them too. Add it as a banner in your bundler config (with terser, use format.preamble so it survives minification), and only on the entries that really need the client. Split anything that works without React into its own entry. For accessly that became accessly/core, which Server Components and Route Handlers can import safely.

The check.

head -c 20 dist/react/index.js   # must print "use client";
Enter fullscreen mode Exit fullscreen mode

Then render a component from the package in a fresh create-next-app project, inside a server layout.

2. require() returned an empty object

The symptom. In Jest or an older SSR setup:

const { SkeletonLoader } = require("react-loadly"); // undefined
Enter fullscreen mode Exit fullscreen mode

The cause. The package had "type": "module", and the CommonJS build was written to index.js. Node decides the format of a .js file from the nearest package.json, so it treated my CommonJS file as an ES module, and require() got nothing useful.

The fix. Give CommonJS files the .cjs extension and their types .d.cts, then point each condition at the right file:

"exports": {
  ".": {
    "import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
    "require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
  }
},
"main": "./dist/index.cjs"
Enter fullscreen mode Exit fullscreen mode

The check.

node -e "console.log(Object.keys(require('./dist/index.cjs')))"
npx @arethetypeswrong/cli --pack .
npx publint
Enter fullscreen mode Exit fullscreen mode

The first line must print your export names. The other two catch wrong types and wrong exports maps across node16, bundler and CommonJS resolution.

3. A 4 MB package for 4 kB of code

The symptom. No error at all, just a package that took 4 MB on every install.

The cause. Without a files field, npm publishes everything that isn't ignored. Mine included the docs site's images. Another package published a full copy of its type declarations in every subpath folder, which turned 32 files into 972.

The fix. An explicit allowlist:

"files": ["dist", "!dist/types", "AGENTS.md", "llms.txt"]
Enter fullscreen mode Exit fullscreen mode

The check.

npm pack --dry-run
Enter fullscreen mode Exit fullscreen mode

Read the file list every time. accessly went from 4 MB to 128 kB, and react-loadly from 972 files to 32.

The habit that ties it together

All three bugs lived in the gap between my repo and a real install. My tests imported the source. My users installed a tarball. So now, before every publish, I:

  • Run npm pack and install the .tgz into a scratch Next.js app and a scratch CommonJS project with Jest.
  • Render one component from a server layout, and require() the package from Jest.
  • Run publint and @arethetypeswrong/cli.- Read the npm pack --dry-run file list.

It takes ten minutes, and it would have caught every one of these.

A bonus for AI coding agents

While fixing these, I also added an AGENTS.md file to each package, with the exact API, when to use the library and the mistakes to avoid. Coding agents read it from node_modules, so they stop guessing method names. If you maintain a package, it's worth an afternoon.

The libraries, if you want to see the setup:

  • react-loadly: loaders, skeletons and the new Skeletonize
  • accessly: permissions for React and Next.js
  • compressly: image compression before upload
  • tonely: UI sounds without audio files

Top comments (0)