A guide: How to build and publish an NPM Typescript package.
Introduction
Have you ever found yourself copy-pasting the same bits of code between different projects?
This was a constant problem for me, so I started developing Typescript packages that allowed
me to reuse useful pieces of code.
This guide will show you step-by-step how to build a package using typescript and published
in the Node Package Manager (npm).
Why use typescript?
Using Typescript will give you a better development experience, it will be your best friend
when developing, always “yelling” at you every single time you make a mistake. In the beginning, you may feel that strong typing decreases productivity and It's not worth it to use. But believe me when I tell you that Typescript has some serious advantages:
Optional Static Typing – Types can be added to variables, functions, properties, etc. This helps the compiler and shows warnings about any potential errors in code before the package ever runs. Types are great when using libraries they let developersknow exactly what type of data is expected.
Intellisense – One of the biggest advantages of Typescript is its code completion andIntellisense. Providing active hints as code is added.
More robust code and easier to maintain.
In my option, Typescript should be your best pal when building packages!Let’s get cooking!
The first step is to create your package folder picking a creative name.mkdir npm-package-guide-project && cd npm-package-guide-projectCreate a git repository.Next, let’s create a remote git repository for your package. How to create a git repository is out of the scope of this article but when you create a repository in GitHub it easily shows you how to do it. Follow the steps there then come back over here!Start your packageAfter the repository is created, you need to create a package.json. It’s a JSON file that resides in the project's root directory. The package.json holds important information. It Contains human-readable metadata about the project, like the project name and description, functional metadata like package version number, scripts to run in the CLI, and a list of dependencies required by the project.npm init -yAfter that, we need to create a .gitignore file at the root of the project. We don’t want unneeded code entering the repository ✋. For now, we only need to ignore the node_modules folder.echo "node_modules" >> .gitignoreGreat Job! This is what the project should look like in Visual Studio Code and in the git repository. From this point on I will continue adding files from vscode.Let’s add in Typescript as a DevDependencyIt will use a more stable version of typescript that is compatible with multiple packages that will be used during this guide.npm install --save-dev typescript@4.7Using the flag –-save-dev will tell NPM to install Typescript as a devDependency. This means that Typescript is only installed when you run npm install, but not when the end-user installs the package. Typescript is needed to develop the package, but it’s not needed when using the package.To compile the typescript, we need to create a tsconfig.json file at the root of the project. This file corresponds to the configuration of the typescript compiler(tsc).
{ "compilerOptions": { "outDir": "./lib", "target": "ES6", "module": "CommonJS", "declaration": true, "noImplicitAny": true }, "include": ["src"], // which files to compile "exclude": ["node_modules", "**/__tests__/*"] // which files to skip}
There are many configuration options in the fields of tsconfig.json and it's important to be aware of what they do.
target: the language used for the compiled output. Compiling to es6 will make our package compatible with browsers.
module: the module manager used in the compiled output.
declaration: This should be true when building a package. Typescript will then also export types of definitions together with the compiled JavaScript code so the package can be used with both typescript and JavaScript.
outDir: Compiled output will be written to his folder.
include: Source files path. In this case src folder.
exclude: What we want to exclude from being compiled by the ts
Let’s Code!
Now with Typescript compilation set up, we are ready to code a simple function that receives parameters and multiplies them, returning the operation result. For this let’s create a src folder in the root and add an index.ts file:
export const Multiplier = (val: number, val2: number) => val * val2;
Then add a build script to package.json:
"build": "tsc"
Now just run the build command in the console:npm run buildThis will compile your Typescript and create a new folder called lib in the root with your compiled code in JavaScript and type definition.It’s needed to add the lib folder to your .gitignore file. Is not recommended for auto-generated files to go to the git remote repository as it can cause unnecessary conflicts.
node_modules/lib
Formatting and linting
A good package should include rules for linting and formatting.
This process is important when multiple people are working/contributing to the same project so that everyone is on the same page when it comes to codebase syntax and style.
Like we did with Typescript, these are tools used only for the development of the package. They should be added as devDependencies.Let’s start by adding ESLint to our package:npm install eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin --save-dev
eslint: ESLint core library
@typescript-eslint/parser: a parser that allows ESLint to understand TypeScript code
@typescript-eslint/eslint-plugin: plugin with a set of recommended TypeScript rules
Similar to Typescript compiler settings, you can either use the command line to generate a configuration file or create it manually in VSCode. Either way, the ESLint configuration file is needed.Create a .eslintrc file in the root:You can use the following starter config and then explore the full list of rules for your ESLint settings.
{ "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": 12, "sourceType": "module" }, "plugins": ["@typescript-eslint"], "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], "rules": {}, "env": { "browser": true, "es2021": true },}
parser: this tells EsLint to run the code through a parse when analyzing the code.
plugins: define the plugins you’re using
extends: tells ESLint what configuration is set to extend from. The order matters.
env: which environments your code will run in
Now let’s add a lint script to the package.json. Adding an --ext flag will specify which extensions the lint will have in account. By default it’s .js but we will also use .ts.
"lint": "eslint --ignore-path .eslintignore --ext .js,.ts ."
There is no need for some files to be linted, such as the lib folder. It’s possible to prevent linting on unnecessary files and folders by creating a .eslintignore file.
node_moduleslib
Now ESLint it’s up and ready! I suggest the integration of ESLint into whatever code editor you prefer to use. In VSCode, go to extensions and install the ESLint extension.To check your code using ESLint you can manually run your script in the command line.npm run lintNow let’s set up PrettierIt’s common the usage of ESLint and Prettier and the same time, so let’s add Prettier to our project:npm install --save-dev prettierPrettier doesn’t need a config file, you can simply run and use it straight away.In case you want to set your own config, you need to create a .prettierrc at the root of your project.If you are curious to know more, I'm leaving here a full list of format options and the Prettier Playground.
//.prettierrc{ "semi": false, "singleQuote": true, "arrowParens": "avoid" }
Let’s add the Prettier command to our scripts. Let’s also support all files that end .ts, .js, and .json, and ignore the same files and directories as .gitignore ( or create a file .prettierignore)
... "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "tsc", "lint": "eslint --ignore-path .eslintignore --ext .js,.ts .", "format": "prettier --ignore-path .gitignore --write \"**/*.+(js|ts|json)\"" },...
Now just run the command npm run format to format and fix all your code.Conflicts with ESLint and Prettier.It’s possible that Prettier and ESLint generate issues when common rules overlap. The best solution here is to use eslint-config-prettier to disable all ESLint rules that are irrelevant to code formatting, as Prettier is already good at it.npm install --save-dev eslint-config-prettierTo make it work you need to go to the .eslintrc file and add it to Prettier at the end of your extends list to disable any other previous rules from other plugins.
// .eslintrc{ "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": 12, "sourceType": "module", }, "plugins": ["@typescript-eslint"], // HERE "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"], "rules": {}, "env": { "browser": true, "es2021": true }
With that, the format and linting section are completed! Awesome job!Setup testing with jestIn my opinion, every package should include unit tests! Let’s add Jest to help us with that.Since we are using Typescript, we also need to add ts-jest and @types/jest.npm install --save-dev jest ts-jest @types/jestCreate a file jestconfig.json in the root:
//jestconfig.json {"transform": {"^.+\\.(t|j)sx?$": "ts-jest"},"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$","moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"]}
Now let’s update the old test script in our package.json file:
//package.json"scripts":{..."test": "jest --config jestconfig.json" ...
Your package.json file should look something like this:Let’s write a basic test! In the src folder, add a new folder named __tests__, and inside, add a file with a name you like, but must end with test.ts, for example, multiplier.test.ts.
//multiplier.test.tsimport { Multiplier } from '../index'test('Test Multiplier function', () => { expect(Multiplier(2, 3)).toBe(6)})
In this simple test, we will pass the numbers 2 and 3 as parameters throw our Multiplier function and expect that the result will be 6.Now just run it.npm testIt works! Nice job! The test passes successfully, as you can see! Meaning that our function is multiplying correctly.Packages.json magic scripts.There are many magic scripts available for use by the Node Package manager ecosystem. It’s good to automate our package as much as possible.In this section we will look at some of these scripts in npm: prepare, prepublishOnly, preversion, version, and postversion.
prepare script runs when git dependencies are being installed. This script runs after prepublish and before prepublishOnly. Perfect for running building code.
"prepare" : "npm run build"
prepublishOnly: this command serves the same purpose as prepublish and prepare but runs only on npm publish!
"prepublishOnly" : "npm test && npm run lint"
perversion: will run before bumping a new package version. Perfect to check the code using linters.
"preversion" : "npm run lint"
version: run after a new version has been bumped. If your package has a git repository, like in our case, a commit and a new version tag will be made every time you bump a new version. This command will run BEFORE the commit is made.
"version" : "npm run format && git add -A src"
postversion: will run after the commit has been made. A perfect place for pushing the commit as well as the tag.
"postversion" : "git push && git push --tags"This is what my package.json looks like after implementing the new scripts:Before PublishWhen we added .gitignore to our project with the objective of not passing build files to our git repository. This time the opposite occurs for the published package, we don’t want source code to be published with the package, only build files.This can be fixed by adding the files property in package.json:
//package.json..."files":[ "lib/**/*" ] ...
Now, only the lib folder will be included in the published package!Final details on package.jsonFinally is time to prepare our package.json before publishing the package:
//package.json{
"name": "npm-package-guide",
"version": "1.0.0",
"description": "A simple multiplier function",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"scripts": {
"test": "jest --config jestconfig.json",
"build": "tsc",
"lint": "eslint --ignore-path .eslintignore --ext .js,.ts .",
"format": "prettier --ignore-path .gitignore --write \"**/*.+(js|ts|json)\"",
"prepare": "npm run build",
"prepublishOnly": "npm test && npm run lint",
"preversion": "npm run lint",
"version": "npm run format && git add -A src",
"postversion": "git push && git push --tags"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Rutraz/npm-package-guide.git"
},
"keywords": [
"npm",
"jest",
"typescript"
],
"author": "João Santos",
"license": "ISC",
...
These final touches to the package.json include adding a nice description, keywords, and an author. It’s important here since it will tell NPM where it can import the modules from.Commit and push your code.The time is here, to push all your work to your remote repository!git add -A && git commit -m "First commit"git pushPublish your package to NPM!To be able to publish your package, you need to create an NPM account.If you don’t have an account you can do so at https://www.npmjs.com/signupRun npm login to login into your NPM account.Then all you need to do is to publish your package with the command:npm publishIf all went smoothly now you can view your package at https://www.npmjs.com/.We got a package! Awesome work!Increase to a new version.Let’s increase the version of our package using our scripts:npm version patchIt will create a new tag in git and push it to our remote repository. Now just published again:npm publishWith that, you have a new version of your package! Congratulations! You made it to the end. Hopefully, you now know how to start building your awesome package!