Skip to content

mod_07 - Publishing Packages

Publishing to npm means your code runs on thousands of machines you'll never see
One npm publish and your library is available to every developer with an internet connection. That's power and responsibility in equal measure. A broken release takes down builds. A malicious release gets reported to npm security. An accidental publish of internal code gets your npm account suspended. Know the mechanics before you hit publish

npm login and authentication

# Login to the npm registry
npm login

# Prompts for:
# Username: 0x1ris
# Password: ********
# Email: mahmoud@example.com
# (One-time password if 2FA is enabled)

# Verify you're logged in
npm whoami

# Check which registry you're publishing to
npm config get registry

# Login with a specific registry (for scoped packages or private registries)
npm login --registry=https://registry.mycompany.com

Authentication tokens

npm supports access tokens for CI/CD - never use your password in scripts

# Generate a publish token (from npm website or CLI)
npm token create
npm token list
npm token revoke <token-id>

# Use token in CI:
# NPM_TOKEN environment variable
# .npmrc: //registry.npmjs.org/:_authToken=${NPM_TOKEN}

Two-factor authentication

Enable 2FA for publishing - non-negotiable if you maintain popular packages

# Enable 2FA on npm (authorization only, or authorization and publishing)
npm profile enable-2fa auth-and-writes

# With 2FA enabled, publishing requires an OTP:
npm publish --otp=123456

npm publish

# Publish the current package
npm publish

# Publish with a tag (default: "latest")
npm publish --tag beta

# Publish a scoped package (requires the scope to match your npm org)
npm publish --access public  # scoped packages are private by default

Before publishing, npm does: 1. Runs prepublishOnly script (if defined) - use this for validation 2. Checks "files" field or .npmignore to determine what gets included 3. Creates the tarball 4. Uploads to the registry

What gets published

npm publishes everything except: * Files in .gitignore (unless .npmignore exists, in which case .npmignore takes precedence) * Certain always-excluded files (.env, .git/, *.orig) * Hidden files starting with . (except .npmignore, package.json, and select others)

Check before publishing:

# Dry run - shows what would be published without actually publishing
npm publish --dry-run

# List files that will be in the tarball
npm pack --dry-run

# Create actual tarball (inspect contents)
npm pack
tar -tvf *.tgz

Always run npm pack --dry-run before your first publish. You don't want to accidentally publish: * .env files with secrets * Test fixtures with mock passwords * Internal documentation * Build artifacts from previous runs * node_modules

.npmignore

If you don't want to use the "files" whitelist approach, use .npmignore

# .npmignore
tests/
src/
*
.env
.DS_Store

This file follows .gitignore syntax. If .npmignore exists , it overrides .gitignore - files ignored by git might be included in the npm package unless .npmignore also ignores them

npm unpublish and deprecate

Unpublishing

You can unpublish a package within 72 hours of publication

# Unpublish a specific version
npm unpublish my-package@1.0.0

# Unpublish the entire package (only within 72 hours of first publish)
npm unpublish my-package --force

Warning: Unpublishing breaks every project that depends on your package. npm has a policy against unpublishing packages that others depend on. After 72 hours , you can't unpublish - only deprecate

Deprecating

Instead of unpublishing , deprecate a version

npm deprecate my-package@1.0.0 "Critical security issue - upgrade to 1.0.1"

Deprecated versions show a warning banner when installed. The package remains available (existing installs don't break) but users see:

npm WARN deprecated my-package@1.0.0: Critical security issue - upgrade to 1.0.1

Always deprecate instead of unpublish for anything older than 72 hours.

scoped packages

Scoped packages live under an npm organization or username

# Publish a scoped package
# package.json: "name": "@my-org/package-name"

npm publish --access public

# Scoped packages default to private - you need --access public to publish publicly

Scoped packages are the standard for: * Organizations with multiple packages (@google-labs/, @aws-sdk/, @angular/) * Company-internal packages on private registries * Preventing name collision on the public registry

{
  "name": "@0x1ris/parser",
  "version": "1.0.0",
  "publishConfig": {
    "access": "public",
    "registry": "https://registry.npmjs.org/"
  }
}

versioning

# Bump version and create a git tag
npm version patch   # 1.0.0 -> 1.0.1 (bug fixes)
npm version minor   # 1.0.1 -> 1.1.0 (new features, backward compatible)
npm version major   # 1.1.0 -> 2.0.0 (breaking changes)

# Custom version
npm version 1.5.0   # sets to exactly 1.5.0

# Pre-release versions
npm version premajor --preid beta   # 1.0.0 -> 2.0.0-beta.0
npm version prerelease --preid beta # 2.0.0-beta.0 -> 2.0.0-beta.1

Each command: 1. Updates the version field in package.json 2. Commits the change to git 3. Creates a git tag (v1.0.1)

Versioning workflow

# For a feature release:
git checkout main
npm version minor          # 1.1.0 -> 1.2.0
npm publish                # push to registry
git push --tags            # push tags to remote

# For a hotfix:
git checkout main
npm version patch          # 1.2.0 -> 1.2.1
npm publish
git push --tags

package.json fields for publishing

"files"

Whitelist what gets published

{
  "files": [
    "dist/",
    "LICENSE",
    "README.md",
    "package.json"
  ]
}

Without this, your entire project directory (except .gitignored stuff) goes to npm

"main"

Entry point for CommonJS consumers

{
  "main": "./dist/index.js"
}

"bin"

Expose executables from your package

{
  "bin": {
    "my-cli": "./bin/cli.js"
  }
}

When installed globally , my-cli becomes available as a system command. When installed locally , npx my-cli works. The referenced file must start with #!/usr/bin/env node

"types"

TypeScript declaration entry point

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

"exports"

Full conditional exports support for ESM and CJS

{
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "require": "./dist/index.cjs",
      "types": "./dist/index.d.ts"
    },
    "./utils": {
      "import": "./dist/utils.js",
      "require": "./dist/utils.cjs"
    }
  }
}

"engines"

Specify Node version compatibility

{
  "engines": {
    "node": ">=18.0.0"
  }
}

README and documentation

Your npm package page is generated from README.md
A good README includes:

  • Installation - npm install your-package
  • Quick start - minimal working example
  • API reference - all exported functions/classes with parameters
  • Examples - real usage patterns
  • Configuration - environment variables , options
  • Security - known security considerations , reporting process

npm renders the first 500KB of README.md on the package page. Put the most important stuff at the top

# My Parser

Zero-dependency parser for log files

## Install
```bash
npm install @0x1ris/parser

Usage

import { parse } from '@0x1ris/parser'
const result = parse('error: connection refused')

API

...

## security: what not to publish

**Before publishing, check your package contents:**

```bash
# Inspect the tarball
npm pack
tar -tvf *.tgz

# Or use the dry run
npm publish --dry-run

Never publish:

  • .env files or any files with secrets , tokens , or passwords
  • node_modules - (npm excludes these automatically , but verify)
  • Test files with mock credentials or internal infrastructure names
  • Source maps that expose original source code
  • Build logs or debug output
  • Internal documentation or architecture diagrams

If you accidentally publish a secret:

# 1. Immediately deprecate the version
npm deprecate my-package@bad-version "Contains sensitive data - do not use"

# 2. Rotate the exposed secret (API key, token, password)
# 3. npm support can unpublish if within 72 hours
# 4. Review what was exposed and audit damage

Reproducible builds

Set the "private": true field in package.json for internal packages as a safety net

{
  "private": true,
  "scripts": {
    "prepublishOnly": "node -e 'console.error(\"This is private\")' && exit 1"
  }
}

This crashes the publish if "private": true is somehow removed

publishing checklist

  • Run npm pack --dry-run and review the file list
  • Run npm audit on your dependencies
  • Run your test suite
  • Run npm run build (if applicable)
  • Check that "main", "exports", and "types" point to correct files
  • Verify .npmignore or "files" excludes everything unnecessary
  • Make sure README.md exists and is up to date
  • Set "private": false (or don't include it)
  • Run npm version <patch|minor|major> to bump
  • Run npm publish (or npm publish --access public for scoped packages)
  • Run git push --tags

summary

  • npm login authenticates you - use tokens for CI , not passwords
  • npm publish --dry-run shows what will be published - always check first
  • Use npm unpublish only within 72 hours - npm deprecate after that
  • Scoped packages (@scope/pkg) are the standard - use --access public
  • npm version patch|minor|major bumps version and creates git tags
  • Set "files" field to whitelist published files - prevent secret leaks
  • prepublishOnly script validates before publish
  • Run npm pack --dry-run before every first publish to a new package
  • Accidental secret publish = deprecate + rotate + audit

prerequisites

mod_06_manage_dep.md - managing dependencies

next -> this is the last file in the async/modules section