How to make an npm package with TypeScript

This is a guide on how to make an npm package with TypeScript. I’ve made a new one recently, so why not write a guide that covers all the gotchas and nuances of writing one?

Initializing your package

You’ll have to start somewhere. If you’re using a package generator like create-react-app, then go ahead and follow its instructions. Otherwise, you can make a new directory and start from scratch. Make sure you’re familiar with the command line of whatever operating system you’re using.

npm init and package.json

You can run npm init on the directory to initialize a package.json file for your package. The package.json file contains the metadata for your package.

When asked for your package’s repository, you can either type owner/project for GitHub projects and gitlab:owner/project for GitLab projects. The project doesn’t need to exist yet.

If you don’t want to publish your package to public online registries like the npm Registry, make sure to put private: true in your package.json file.

Git

It’s a good idea to keep your code under version control. A good version control software is Git. To initialize your package under Git, run git init.

.gitignore

For npm packages, it’s a good idea to create a .gitignore file.

Generally, you would want to keep your dependencies out of version control. For this, add the line node_modules/ to your .gitignore file.

Additionally, you might want to exclude operating system files from version control. For example, Windows generates a Thumbs.db file for any folder with images. To exclude it, add **/Thumbs.db to your .gitignore file.

Additional files to gitignore (not required):

  • *.tgz (npm pack output)
  • npm-debug.log (recent versions of npm don’t output this in the package directory anymore)

If you have an .npmignore file too, make sure they have the same files commonly ignored too.

License

It’s a good idea to include licensing terms with your package. When asked for the license in npm init or editing your package.json file, you can add an SPDX expression containing the license you want. You should additionally paste the whole text of the license in a license.md/license.txt/LICENSE file in your package directory (editing in your name/the year/your company).

If you do not wish to grant others rights to your package, you can simply type in UNLICENSED. This is not to be confused with The Unlicense license. Upon doing so, you might want to set private: true too and make sure you’re not pushing the code to a public Git repo.

For guidance on what license to choose, refer to choosealicense.com.

Where to put your TypeScript files?

There’s no prescribed way on where to put TypeScript files (package root vs. src) although now I prefer src as this excludes any stray .ts files in node_modules. When doing this, make sure the main field in your package.json is updated.

Make sure to refer to your compiled files with the .js extension.

tsconfig.json

Refer to the tsconfig.json documentation.

As of the time of writing, many authors still write their packages in CommonJS style. To have CommonJS outputs, set compilerOptions.module to commonjs.

Excluding .ts files

In as much as we want to avoid stray .ts files in node_modules when compiling our source code, we don’t want others to compile their code with our .ts files in their node_modules. To do this, we will have to exclude our own .ts files from the published code.

File.gitignore.npmignoreNotes
.ts(do not ignore)src/**/*.ts
.jssrc/**/*.js(do not ignore)
.js.mapsrc/**/*.js.map(do not ignore)with the sourceMap compiler option set
.d.tssrc/**/*.d.ts!src/**/*.d.tswith the declaration compiler option set
.d.ts.mapsrc/**/*.d.ts.map(do not ignore)with the declarationMap compiler option set

(Where src is the root TypeScript directory. If using the package root, simply remove src/ from the beginning.)

A separate set of .gitignore and .npmignore files can be placed in the src directory. Note that on each directory with a .gitignore file, the set of files npm ignores are also reset, so you’d need a new .npmignore file to set them again.

build and watch scripts.

It’s a good idea to add build and watch scripts to your package.json scripts field.

{
  "scripts": {
    "build": "tsc --project src",
    "watch": "tsc --project src --watch"
  }
}

Keeping the arguments in full form is purely aesthetic.

Make sure to also put a prepublishOnly script so that you don’t accidentally publish an unbuilt package.

{ "prepublishOnly": "npm run build" }

Better if you have a test script:

{
  "pretest": "npm run build",
  "test": "node src/test.js",
  "prepublishOnly": "npm test"
}

This will make sure your project is built and it passed your test before being published.

Test script

It is recommended to have a test script. Write your test script however you want. If the test script is also written in TypeScript, it’s good to have only one TypeScript root directory.

Include in published package?

Test scripts do bloat your package unnecessarily. Still, try to include them in the published version so that users can check if your package works in their environment.

Editor-specific folders

If you’re working with an IDE or editor, you would want their specific files checked in source control but excluded from your published code.

VS Code: .vscode/settings.json

It’s useful to hide TypeScript-generated code from search and your file tree. Fortunately, Visual Studio Code allows for glob patterns unlike .gitignore.

{
  "files.exclude": {
    "src/**/*.{js,d.ts}{,.map}": true
  },
  "search.exclude": {
    "package-lock.json": true,
    "src/**/*.{js,d.ts}{,.map}": true
  }
}

VS Code: prelaunch task

You can define a pre-launch task that will run before debugging, but you have to define the task first in your .vscode/tasks.json file. If the task is a watcher, it needs to be able to tell when it has run once so that VS Code knows when to proceed to run the debugger. Fortunately, this behavior is already defined for tsc --watch.

.vscode/tasks.json

{
  // See https://go.microsoft.com/fwlink/?LinkId=733558
  // for the documentation about the tasks.json format
  "version": "2.0.0",
  "tasks": [
    {
      "type": "npm",
      "script": "watch",
      "problemMatcher": "$tsc-watch",
      "isBackground": true,
      "presentation": {
        "reveal": "never"
      },
      "group": {
        "kind": "build",
        "isDefault": true
      }
    }
  ]
}

.vscode/launch.json

{
  // Use IntelliSense to learn about possible attributes.
  // Hover to view descriptions of existing attributes.
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "test",
      "program": "${workspaceFolder}/src/test.js",
      //"outputCapture": "std",
      //"cwd": "${workspaceRoot}",
      "preLaunchTask": "npm: watch"
    }
  ]
}

outputCapture can be set to std if for some reason your logging facility doesn’t use console.log.

CI configuration

For your choice of Docker image to use in a CI server, if your package or any of its dependencies do not need complicated stuff like a whole browser, try to make use of an image based on Alpine Linux. Alpine Linux is a very minimal version of Linux. This practice will make your build times much shorter.

Building a test matrix

For packages with tests, it’s common to test against multiple versions of node. Consult your CI documentation for details.

Docker images for node

Useful tags for the node image that follow updated versions include:

  • alpine / current-alpine
  • current / latest
  • lts-alpine
  • lts
  • chakracore

For an image with git check out my image: seangenabe/node-git:latest.

This is just:

FROM node:alpine
RUN apk add --no-cache bash git openssh

For packages with native modules you might need to apk add --no-cache make gcc g++ python for node-gyp.

Gitlab CI/CD: .gitlab-ci.yml

Making a Gitlab CI/CD config file is pretty straightforward.

Without node_modules caching:

# cache:                      # With caching
# paths:
# - node_modules/

build:
  image: node:alpine
  stage: build
  script:
    - npm ci --no-audit

    # - npm ci --no-audit -d    # Verbose

    # - npm install --no-audit  # With caching
    # - npm prune

    # - npm ci                  # With security auditing

    - npm test

On security audits

Security audits are first and foremost for people who will be concerned if there are unsecure dependencies in your project. If it’s just you individually, you can just run npm audit. If otherwise you’re failing the CI build for introducing an unsecure dependency, go ahead.

Publishing flow

  • git commit
  • npm version
  • npm publish
  • git push

Running npm version before git push means you’ll have to push only once.