Master the Craft

Becoming a proficient developer often involves not just using packages, but also creating and sharing your own. This interactive guide will walk you through the process of creating and publishing your own NPM packages, from a simple JavaScript module to a more robust TypeScript package with enhanced security through provenance. Use the navigation on the left to jump to any section.

The Package Lifecycle

Before we dive in, it's helpful to understand the high-level process of bringing a package to life. The journey typically follows these steps, which we'll explore in detail throughout this guide.

Initialize
Code
Build (TS)
Publish
Add Provenance

How to Create a JavaScript Package

Let's start with the fundamentals: a straightforward JavaScript package. This is the most common and simplest type of package to create, forming the foundation of the NPM ecosystem.

1. Initialize Your Project

First, create a new directory for your package, navigate into it, and initialize a new NPM project. The `npm init -y` command quickly creates a `package.json` file with default values.

bash
mkdir my-js-package && cd my-js-package
npm init -y

2. Fine-Tune Your `package.json`

Your `package.json` file holds the metadata for your project. Update the default fields to accurately describe your package. Key fields include `name`, `version`, `description`, `main`, and `license`.

json
{
  "name": "my-awesome-greeter",
  "version": "1.0.0",
  "description": "A simple package to greet users.",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": ["greeting", "hello", "welcome"],
  "author": "Your Name <you@example.com>",
  "license": "MIT"
}

3. Write Your Code

Create the file specified as your `main` entry point. In this case, it's `index.js`. This file will contain the core logic of your package.

javascript
// index.js
function sayHello(name) {
  return `Hello, ${name}! Welcome to the world of NPM packages.`;
}

module.exports = { sayHello };

4. Test It Locally

Before you publish, always test your package on your local machine. The `npm link` command creates a global symbolic link, allowing you to use your package in other local projects as if it were installed from the registry.

bash
npm link

How to Create a TypeScript Package

For more complex projects, TypeScript offers the benefits of static typing, improving code quality and maintainability. Setting up a TypeScript package involves a build step to compile your code into JavaScript.

1. Initialize and Add Dependencies

Start by creating a new project and initializing it. Then, install TypeScript as a development dependency.

bash
mkdir my-ts-package && cd my-ts-package
npm init -y
npm install typescript --save-dev

2. Configure TypeScript

Generate a `tsconfig.json` file. This configuration file tells the TypeScript compiler how to handle your project, including where to find source files and where to output the compiled JavaScript.

bash
npx tsc --init

For a library, you should configure `tsconfig.json` to generate type declaration files (`.d.ts`) and output compiled code to a `dist` directory.

json
{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "declaration": true,
    "outDir": "./dist",
    "strict": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

3. Update `package.json` for TypeScript

Adjust `package.json` for TypeScript. Point `main` to the compiled output, add a `types` field for type definitions, include a `build` script, and use the `files` property to ensure only the `dist` folder gets published.

json
{
  "name": "my-typed-greeter",
  "version": "1.0.0",
  "description": "A simple TypeScript package to greet users.",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "tsc"
  },
  "files": ["dist"],
  "author": "Your Name <you@example.com>",
  "license": "MIT",
  "devDependencies": {
    "typescript": "^5.0.0"
  }
}

4. Write Your TypeScript Code

Create a `src` directory and your `index.ts` file within it. This is where your typed source code will live.

typescript
// src/index.ts
export function sayHello(name: string): string {
  return `Hello, ${name}! Welcome to the world of TypeScript NPM packages.`;
}

5. Build Your Package

Run your build script to compile the TypeScript code into JavaScript. This creates the `dist` directory with the files that will actually be distributed via NPM.

bash
npm run build

Publishing Your Package

With your package created and tested, it's time to share it with the world. Publishing makes your package available for anyone to install and use in their own projects.

1. Log in to NPM

First, you'll need an NPM account. If you don't have one, create one at npmjs.com. Then, log in from your terminal:

bash
npm login

2. Publish

Navigate to your package's root directory and run the `publish` command. If the package name is unique, it will be uploaded to the NPM registry.

bash
npm publish

Publishing Scoped Packages

Scoped packages are a great way to namespace your work, especially for organizations. They have the format `@scope/package-name`. By default, your scope is your NPM username. To publish a public scoped package for free, use the `--access public` flag.

bash
npm publish --access public

How to Provide Provenance

Package provenance provides a verifiable link between a published package, its source code, and its build instructions, significantly enhancing supply-chain security. This assures consumers that the package they are installing was built from a trusted source.

Prerequisites

  • Your project must be in a public Git repository.
  • You need to use a supported CI/CD provider like GitHub Actions.

Configure Your CI/CD Workflow

To publish with provenance, modify your CI/CD workflow. In your GitHub Actions workflow file, grant `id-token: write` permissions and add the `--provenance` flag to your `npm publish` command. Once published, your package page on npmjs.com will display a "Verified" badge.

yaml
# .github/workflows/publish.yml
name: Publish Package with Provenance

on:
  push:
    branches:
      - main

jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      id-token: write # This is required for provenance
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20.x'
          registry-url: 'https://registry.npmjs.org'

      - run: npm ci
      - run: npm run build # If you have a build step
      - run: npm publish --provenance
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}