Publishing your first npm library
Tags: npm, node, javascript, beginners
Published: 2024-04-24 23:49:22

## Introduction
I recently published my [first npm library called `pushdown-automaton`](https://www.npmjs.com/package/pushdown-automaton) which allows users to create [Pushdown Automata](https://en.wikipedia.org/wiki/Pushdown_automaton). And while it is a very niche use-case I am still proud of my achievement.
This article's purpose is highlighting anything important I ran into to help everyone reading this.

## Librarification of your code
Personally, I started off with something like this:
```
/
  PushdownAutomaton.js
  Stack.js
  State.js
  TerminationMessage.js
  TransitionFunction.js
  package.json
  .gitignore
  .tool-versions
```
And while this is fine to hand in for a school project, there is still a lot missing to turn it into a library.

## Using typescript (optional)
First, the project should be converted to TypeScript. That makes using the library as an end-user much easier, as there are type-errors in case someone uses it wrong:
![Screenshot of a type error when using the library wrong](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1omnmi06zdqbue5esmhp.png)

### Coverting your files
Firstly you should change all `*.js` files to `*.ts`.
Then you need to add types everywhere:
```typescript
let automaton: PushdownAutomaton;
automaton = new PushdownAutomaton("test");

let oneState: State;
oneState = new State("q0");
let otherState: State;
otherState = new State("q1");
```
While I did it manually, you can probably just feed all your files into ChatGPT and make it do the manual labor. Just use at your own discretion.

### Changing folder structure
To make everything more readable and easier to understand you might want to move the source `*.ts` files into their own folder:
```
/
  src/
    PushdownAutomaton.ts
    Stack.ts
    State.ts
    TerminationMessage.ts
    TransitionFunction.ts
  package.json
  .gitignore
  .tool-versions
```
Later we will set up an `out/` folder that holds our end-user code.

### Setting up the ts compiler
As we still want to make the library usable for non-ts users we have to add the `tscompiler` that turns our code into JavaScript.
As we only need it when developing and not when sending our package to the user, make sure to only install it in development:
```bash
npm install --save-dev typescript
```
And now we define a few commands in our `package.json` that make compilation easier:
```json
"scripts": {
  "build": "tsc --outDir out/",
},
```
This allows us to just run `npm run ...` and have it compile directly into the correct directory. Now running any of those commands doesn't work as of now:
```bash
➜ npm run build

> pushdown-automaton@1.1.3 build
> tsc --outDir out/

Version 5.4.5
tsc: The TypeScript Compiler - Version 5.4.5
                                                                                                                 
COMMON COMMANDS
...
```
#### TypeScript config
This happens, as we don't yet have a typescript config set up.
Luckily, we can generate one by running:
```bash
➜ npx tsc --init

Created a new tsconfig.json with:
                                                                                                                 TS
  target: es2016
  module: commonjs
  strict: true
  esModuleInterop: true
  skipLibCheck: true
  forceConsistentCasingInFileNames: true
```
And the generated `tsconfig.json` might look like this:
```json
{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  }
}
```
And while this works, it's not quite what we want. After changing it around a bit, this one looked pretty good for me:
```json
{
  "compilerOptions": {
    /* Basic Options */
    "target": "es6",
    "module": "ESNext",
    "lib": ["es6", "dom"],
    "declaration": true,
    "sourceMap": true,
    "outDir": "./out",

    /* Strict Type-Checking Options */
    "strict": true,

    /* Module Resolution Options */
    "moduleResolution": "node",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,

    /* Advanced Options */
    "skipLibCheck": true
  },
  "exclude": ["node_modules", "test", "examples"],
  "include": ["src/**/*.ts"]
}
```
Important settings are:
- target: This is the JS Version our files will be transpiled to
- module: This defines the module system our code will use. `ESNext` allows for keywords like `import` and `export`
- lib: This defines what will be included in our compilation environment
- declaration: This option tells the compiler to create declaration files (explained better under the chapter `*.d.ts`)
- sourceMap: This option tells the compiler to create sourcemap files (explained better under the chapter `*.js.map`)
- outDir: Where our files are sent to (if nothing is specified in the command)
- include: What glob pattern to use when searching for files to be compiled

Now we can re-run our commands sucessfully:
```bash
➜ npm run build

> pushdown-automaton@1.1.3 build
> tsc --outDir out/
```
Inside of the `out/` folder you should now see a bunch of files, having following endings:

##### `*.js`
These are JavaScript files. They contain the actual code.

##### `.d.ts` 
These are Type declarations: These files tell any TypeScript compilers about types, etc. giving them the ability to catch type errors before runtime.
The content looks like a Java `interface`:
```typescript
declare class PushdownAutomaton {
    //...
    run(): TerminationMessage;

    step(): TerminationMessage;

    setStartSate(state: State): void;
    //...
}
```

##### `.js.map`
These files are used by the browser to allow users to see the original files instead of the compiled ones. Reading them doesn't make much sense, as they are just garbage.

#### ESNext issues when using TypeScript
If you already tried using your library you might have realized that nothing works. That is for one simple reason: TypeScript imports don't get `.js` added after filenames with `tsc`:
```typescript
// This import in ts:
export { default as PushdownAutomaton } from './PushdownAutomaton';

// Gets turned into this in js:
export { default as PushdownAutomaton } from './PushdownAutomaton';

// While this is needed:
export { default as PushdownAutomaton } from './PushdownAutomaton.js';
```
To fix that, I used some random npm package I found, called `fix-esm-import-path`.
Automating the process of using this needs us to add more `scripts` in our `package.json`:
```json
"scripts": {
  "build": "npm run build:compile && npm run build:fix",
  "build:fix": "fix-esm-import-path out/*.js",
  "build:compile": "tsc"
}
```

### Reflecting the changes in our `package.json`
We made many structural changes to our project, we need to change the `package.json` by adding an indicator for the type of project we have and where our files are:
```json
{
  "files": ["out/**/*"],
  "type": "module"
}
```

### Adding JSDocs
JavaScript supports something called "JSDocs". They are those helpful messages you sometimes see when using a function:
![Image of a jsdoc help message](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4qmhvetydu3j1bbyliyc.png)
Adding these docs to every method and class will increase the usability by a lot, so I would suggest you do that.

## Creating the "entry point" 
When someone uses our package now, that person would expect to import our libraries code like this:
```js
import { PushdownAutomaton, State, TransitionFunction } from 'pushdown-automaton';
```
But as of now that isn't possible. They would have to do this:
```bash
import PushdownAutomaton from 'pushdown-automaton/out/PushdownAutomaton'
```
To enable this first type of imports we will create something called an entry point. That file is located under `src/index.ts` and looks like this:
```ts
export { default as PushdownAutomaton } from './PushdownAutomaton';
export { default as Stack } from './Stack';
export { default as State } from './State';
export { default as TransitionFunction } from './TransitionFunction';
```
All this does is just bundle up everything the user needs. Configuring it like this increases ease of use.

### Setting that in our `package.json`
Now we need to define the entry point in our `package.json` file:
```json
{
  "main": "out/index.js",
  "types": "out/index.d.ts",
}
```
All this does is tell the end-user where to find the "entry point" and its types.

## Clean code and testing (optional)
Most libraries make use of things like linters and tests to guarantee maintainability and expand-ability. 
While this is not needed, I always advocate for it. It makes the development experience for you and any potential future maintainers much better.

### Clean code check
First, we want to set up `eslint`, which is a JavaScript library that allows us to check for certain clean-code standards and if we are following them.

#### Installing packages
We will start by installing a few packages:
```bash
npm install --save-dev eslint @eslint/js typescript-eslint
```

#### Configuring eslint
Next, we will create a file called `eslint.config.mjs`. It will be pretty empty, only having following content:
```js
// @ts-check

import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';

export default tseslint.config(
  eslint.configs.recommended,
  ...tseslint.configs.recommended,
);
```
This just takes what clean-code rules are popular at the moment and enforces them.

### Testing
Next, we will set up `jest` in combination with `istanbul` to check coverage.

#### Installing
With following command you can install `jest`, which also contains `istanbul`:
```bash
npm install --save-dev jest ts-jest @types/jest
```

#### Configuring
To configure `jest` you can add following content to your `jest.config.mjs`:
```js
// jest.config.mjs
export default {
  preset: "ts-jest",
  testEnvironment: "node",
  roots: ["<rootDir>/tests"],
  testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$",
  moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
  collectCoverage: true,
  coverageDirectory: "coverage",
  coverageReporters: ["text", "lcov"],
  coverageThreshold: {
    global: {
      branches: 100,
      functions: 100,
      lines: 100,
      statements: 100,
    },
  },
};
```
Now we can run our tests and see the coverage listed of every mentioned file:
![Image of 100% coverage](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/riur0po6sdz1cdqsb6rs.png)

The config a few interesting options, which you can look up yourself. Important are following options:

##### roots
This defines where the tests are. In this case they are under `/tests/`.

##### testRegex
This defines the syntax filenames of tests have to follow. This regex enforces the format `something.test.ts` but also allows similar names like `something.spec.tsx`.

##### coverageTheshold
This defines what percentage of lines have to be touched by our tests. In this case all options are set to 100%, which enforces complete test coverage.

### Adding scripts
After adding and configuring both a linter and tests, we need to have a standard way of running them.
That can be achieved by adding following options to our `package.json`:
```json
{
  "scripts": {
    "test": "jest --coverage",
    "lint": "eslint 'src/**/*.ts'",
    "fix-lint": "eslint 'src/**/*.ts' --fix",
  }
}
```

## Automated testing and git hooks (optional)
To make enforcing of code-quality easier we will add git-hooks and GitHub actions to run our linter and tests.

### git hooks
To help us with adding git hooks, we will use husky:
```bash
npm install --save-dev husky
```
Luckily `husky` has tools to help us with setting up the hook:
```bash
npx husky init
```
This adds following things:
- A pre-commit script under `.husky`
- Adds `prepare` in `package.json`

Finally, we can add our linter under `.husky/pre-commit`:
```bash
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

echo "Running pre-commit hooks..."


# Run ESLint
echo "Linting..."
npm run lint
if [ $? -ne 0 ]; then
  echo "ESLint found issues. Aborting commit."
  exit 1
fi

echo "Pre-commit checks passed."
```
Now it runs the linter before every commit and forbids us from finishing the commit if there are any complaints by `eslint`. That might look like this:
![Image of a commit with hooks](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nehcinod1yock4epcq1c.png)

### Setting up GitHub actions
Now we want to set up GitHub actions so it runs our tests and lints on every push.
For that, we will create `.github/workflows/tests.yml`. In there we define the workflow:
```yml
name: Tests on push

on:
  push:
    branches:
      - '**'

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [16.x, 18.x]
        os: [ubuntu-latest, windows-latest, macos-latest]

    steps:
    - uses: actions/checkout@v3
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}
        registry-url: 'https://registry.npmjs.org'

    - name: Cache node modules
      uses: actions/cache@v2
      with:
        path: ~/.npm
        key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
        restore-keys: |
          ${{ runner.os }}-node-

    - name: Install dependencies
      run: npm ci

    - name: Lint Code
      run: npm run lint

    - name: Run Tests
      run: npm test
```
This runs our tests on ubuntu, windows and macos on versions 16 and 18 of `node`.
Feel free to change the matrix!
![Image of successful checks](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/btrlt01rfc1h37ln72nx.png)

## Publishing a package
Finally, we can publish our package. For that we need to create an account under [npmjs.com](https://www.npmjs.com).

### Final settings
Some final things we will want to configure before uploading are in our `package.json`:
```json
{
  "name": "Some name",
  "version": "1.0.0",
  "description": "Some description",
  "repository": {
    "type": "git",
    "url": "git+ssh://git@github.com/user/repo"
  },
  "keywords": [
    "some",
    "keywords"
  ],
  "author": "you, of course :)",
  "license": "MIT I hope",
  "bugs": {
    "url": "https://github.com/user/repo/issues"
  },
  "homepage": "https://github.com/user/repo#readme"
}
```

Also, we will want to create a file called `CHANGELOG.md` and reference it in our `README`. The file looks as follows for now:
```markdown
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.0] - yyyy-mm-dd
- Initial release
```
Check out [how to keep a changelog](https://keepachangelog.com/en/1.1.0/) and [Semantic Versioning](https://semver.org/spec/v2.0.0.html) to always keep your library understandable.

### Manual publish
To publish the package manually, we can do that in our console.
First we log in by running:
```bash
npm adduser
```
That will open the browser window and ask us to log in.
After doing that you can run:
```bash
npm run build; npm publish
```

### Automating that work
If you want to automate this work we can configure a GitHub action to automatically publish on npm when pushing a new tag.

#### YAML config to publish
With following file under `.github/workflows/publish.yml` a new release gets triggered on every new tag.
Special about this file is also, that it makes sure our `package.json` has the same version for our package as the pushed tag.
```yml
name: Publish to npm registry

on:
  push:
    tags:
      - '**'

jobs:
  check-tag-version:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Check if tag matches version in package.json
        run: |
          TAG_NAME=${GITHUB_REF#refs/tags/}
          PACKAGE_VERSION=$(jq -r '.version' package.json)
          if [ "$TAG_NAME" != "$PACKAGE_VERSION" ]; then
            echo "::error::Tag version ($TAG_NAME) does not match version in package.json ($PACKAGE_VERSION)"
            exit 1
          fi

  check-code:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18.x'
          registry-url: 'https://registry.npmjs.org'

      - name: Cache node modules
        uses: actions/cache@v2
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-

      - name: Install dependencies
        run: npm ci

      - name: Lint Code
        run: npm run lint

      - name: Run Tests
        run: npm test

  publish:
    needs: [check-tag-version, check-code]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18.x'
          registry-url: 'https://registry.npmjs.org'

      - name: Cache node modules
        uses: actions/cache@v2
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-

      - name: Install dependencies
        run: npm ci

      - name: Build the project
        run: npm run build

      - name: Publish to npm
        run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```

#### Generating an access token
After adding this, you will need to add an npm auth token to your GitHub Actions environment variables.
Get that key under "Access Tokens" after clicking on your profile picture. Generate a "Classic Token".
On that page, add a name and choose "Automation" to allow managing the package in our CI
![Image of access token generation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tws6i8ecny2q8r4z2cqz.png)

#### Adding that token
To now add that token to GitHub Actions secrets.
You can find that setting under (Project) Settings > Secrets and variables > Actions.
Then click on "New repository secret" and add `NPM_TOKEN`:
![Image of an added env key](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cw97q7lyasuqntxexga9.png)

#### Testing our automated publish
If we did everything correctly a new tag should trigger the "publish" action, which automatically publishes:
![Image of the automatic publish](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/waottr9nrmlchft6jzfc.png)

## Conclusion
Now you can finally check out your own npm package on the official website. Good job!
![Image of a list with npm packages](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kpnybdbc0ebjfyvqovup.png)