
February 04, 20257 min read
The file I keep forgetting to add until npm tells me my package is way bigger than I expected and I look at what’s actually in there.
I’ve shipped enough small npm packages to know the moment well: I run npm publish, watch the upload counter climb, and realize the tarball is way bigger than the actual code I’m shipping. I once shipped a 600MB package by accident – the entire node_modules, build artifacts for three operating systems, the works. npm pack --dry-run would have caught all of it. I didn’t run it.
This is what .npmignore is for. It’s a file in your project root that tells npm what to leave out of the published package. Same syntax as .gitignore: glob patterns, one per line. npm docs cover the basics, but the file most people end up with looks like the one below.
Here’s the .npmignore I copy into most TypeScript packages. It’s grouped by what each block is for so I can comment things in or out per project.
# =========================
# Node dependencies & locks
# =========================
node_modules/
package-lock.json
yarn.lock
pnpm-lock.yaml
# ===========
# Build output (intermediate; you ship dist/ but not these)
# ===========
coverage/
.nyc_output/
# ======================
# TypeScript source & configs
# (only ignore src/ if you're shipping compiled dist/)
# ======================
src/
tsconfig.json
tsconfig.*.json
.tsbuildinfo
# ======================
# Environment variables
# ======================
.env
.env.*
# ========
# Testing
# ========
test/
__tests__/
__mocks__/
*.test.ts
*.spec.ts
*.test.js
*.spec.js
# ===========
# Logs & cache
# ===========
*.log
logs/
*.cache
# ==============================
# OS- or editor-specific files
# ==============================
.DS_Store
Thumbs.db
*.swp
.vscode/
.idea/
A note on src/: only ignore it if you’re publishing the compiled dist/ output. If you ship source directly (as some libraries do, especially for tree-shaking or to avoid build steps), don’t ignore src/ – that is your package.
files field is usually a better defaultHonestly, for newer projects I reach for the files field in package.json instead. It’s the inverse approach: instead of listing what to exclude, you list what to include. Anything not in the allowlist doesn’t ship.
{
"name": "my-package",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist",
"README.md"
]
}
Why I prefer it: the failure mode is safer. Forgetting a line in .npmignore means accidentally publishing something you didn’t mean to (an .env file, a token, internal notes). Forgetting a line in files means a missing artifact that’s immediately obvious when someone tries to install your package.
Note: package.json, README, LICENSE, and CHANGELOG files are always included regardless of what you put in files. You don’t need to list them.
.npmignore is still useful when you have a specific subdirectory you want included except for a few patterns inside it – the allowlist is too coarse for that. The two also interact: if a files field exists, .npmignore is ignored. Pick one and stick with it per project.
Whichever approach you pick, run this before the actual publish:
npm pack --dry-run
It prints the list of files that would be in the tarball. Scan it. If you see anything you didn’t expect (an .env, a .git directory, a half-gigabyte test fixture), fix the ignore rules and run it again. Five seconds of paranoia here has saved me from publishing things I really shouldn’t have.
If you want to verify a package that’s already been published, npm view <package> dist.tarball gives you the tarball URL, and you can tar tzf it to see what’s actually in there. Useful when auditing dependencies, too.
Discussion
Comments are powered by Disqus. Sign in once, comment anywhere.
