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 | .npmignore | Notes |
---|---|---|---|
.ts | (do not ignore) | src/**/*.ts | |
.js | src/**/*.js | (do not ignore) | |
.js.map | src/**/*.js.map | (do not ignore) | with the sourceMap compiler option set |
.d.ts | src/**/*.d.ts | !src/**/*.d.ts | with the declaration compiler option set |
.d.ts.map | src/**/*.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.