Building an NPM TS package

来都来了 2021-01-10 10:49:02 ⋅ 509 阅读

 

劳而不伐,有功而不德,厚之至也。(《周易·系辞上》)

原文链接--Step by step: Building and publishing an NPM Typescript package.

Introduction

In this guide, we will build a reusable module in Typescript and publish it as a Node.js package. I’ve seen it being done in many different ways so I want to show you how you can use the best practices and tools out there to create your own package, step by step using Typescript, Tslint, Prettier and Jest.
This is what we are going to build:

https://www.npmjs.com/package/my-awesome-greeter
https://github.com/caki0915/my-awesome-greeter

What is NPM?

Npm is the package manager for Javascript and the world biggest library of reusable software code. It’s also a great as build tool itself as I will show later on.

Why Typescript?

As a superset to Javascript, Typescript provides optional typing and deep intellisense [智能]. When it comes to package development, this is my personal opinion:

I believe that all packages should be built in Typescript

Some of you might feel that strong typing decreases [减少-dɪˈkriːs] productivity[生产效率-prɒdʌkˈtɪvəti] and it’s not worth the effort [努力-ˈefət] to use. I can agree when it comes to small-scale projects, however, when it comes to package-development, Typescript has some serious advantages:

  1. More robust[强大的rəʊˈbʌst] code and easier to maintain[保持-维持meɪnˈteɪn].

  2. The package can be used both for Typescript and Javascript users! If your library becomes popular there will sooner or later be a demand for type-definitions, and to write those manually is time-consuming, error-prone and harder to update.

  3. With type-definitions in the package, the user doesn’t have to download the types from another package.

  4. Strong typings are more self-documenting and makes the code more understandable.

  5. Even if the one using your package doesn’t use Typescript, some editors, like Visual Studio Code will still use the type-definitions to give the user better intellisense.

Alright. Let’s get started!

Make sure you have the latest version of node and npm.

node -v
10.0.0
npm -v
6.0.0

Pick a great name

This might be harder than it sounds. Package names has to be in pascal-case and in lowercase. Since there are 700k+ packages, make a quick search on https://www.npmjs.com/ to make sure your awesome name is not already taken. For the sake[目的、清酒-seɪk] of this guide, I will choose the name my-awesome-greeter, but use a unique name so you can publish your package to npm later on 😉.

Basic Setup

Create your package folder with a suitable name

> mkdir my-awesome-greeter && cd my-awesome-greeter

Create a git repository

First thing first. You need a remote git repository for your package so it can be downloaded. Creating a remote git repository is out of scope for this article but once you have done it you can use the following lines to initialize your local repository and set your remote origin.

git init
echo "# My Awesome Greeter" >> README.md
git add . && git commit -m "Initial commit"
Replace <Git Repository Url> with the URL to your remote repository.
git remote add origin <Git Repository Url>
git push -u origin master

Init your Package

Let’s create a package.json file with all default values.

We’re going to modify this one later on.

npm init -y

As the last step, we’re going to add a .gitignore file to the root. There’s a lot .gitignore templates out there but I like to keep it simple and don’t add more than you need. At the moment, we only need to ignore the node_modules folder.

echo "node_modules" >> .gitignore

Awesome! We got the basics 😃 This is how it looks like when I open the project in Visual Studio Code. From now on I will continue adding files from vscode from now on rather than using the console, but choose a style that suits you 😉
Image for post

Add Typescript as a DevDependency

Let’s start with typescript as a dependency

npm install --save-dev typescript

The flag --save-dev will tell NPM to install Typescript as a devDependency. The difference between a devDependency and a dependency is that devDependencies will only be installed when you run npm install, but not when the end-user installs the package.

For example, Typescript is only needed when developing the package, but it’s not needed while using the package.

Good! Now you will see a node_modules folder and a package-lock.json in your root as well.

In order to compile Typescript we also need a tsconfig.json file so let’s add it to the project root:

{
  "compilerOptions": {
    "target""es5",
    "module""commonjs",
    "declaration"true,
    "outDir""./lib",
    "strict"true
  },
  "include": ["src"],
  "exclude": ["node_modules""**/__tests__/*"]
}

 

A lot of things is going on here, so let’s explain our config file:

  • target: We want to compile to es5 since we want to build a package with browser compatibility.

  • module: Use commonjs for compatibility[兼容性-kəmˌpætəˈbɪləti].

  • declaration: When you building packages, this should be true. Typescript will then also export type definitions together with the compiled javascript code so the package can be used with both Typescript and Javascript.

  • outDir: The javascript will be compiled to the lib folder.

  • include: All source files in the src folder

  • exclude: We don’t want to transpile node_modules, neither tests since these are only used during development.

Your first code!

Now when we have the compilation set up, we can add our first line of code.
Let’s create a src folder in the root and add an index.ts file:

export const Greeter = (name: string) => `Hello ${name}`

Ok, it’s a good start. Next step is to add a build script to package.json:

"build" : "tsc"

 

Now you can run the build command in the console:

npm run build

And violá!
You will see a new lib folder in the root with your compiled code and type definitions!

 

Ignore compiled code in git

Except for package-lock.json, you normally don’t want to have auto-generated files under source control. It can cause unnecessary conflicts, every time it’s is autogenerated. Let’s add the lib folder to .gitignore:

node_modules
/lib

The slash before lib means “Ignore only the lib folder in the top of the root” This is what we want in this case.

Formatting and Linting

An awesome package should include strict rules for linting and formatting. Especially if you want more collaborators later on. Let’s add Prettier and TsLint!

Like Typescript, these are tools used only for the development of the package. They should be added as devDependencies:

npm install --save-dev prettier tslint tslint-config-prettier

tslint-config-prettier is a preset we need since it prevents conflicts between tslint and prettiers formatting rules.
In the root, add a tslint.json:

{
   "extends": ["tslint:recommended""tslint-config-prettier"]
}

And a .prettierrc

{
  "printWidth"120,
  "trailingComma""all",
  "singleQuote"true
}

Finally, add the lint- and format scripts to package.json

"format""prettier --write \"src/**/*.ts\" \"src/**/*.js\"",
"lint""tslint -p tsconfig.json"

Your package.json should now look something like this:

{
  "name""w-toolkit",
  "version""1.0.1",
  "description""Node 工具包",
  "main""lib/index.js",
  "types""lib/index.d.ts",
  "scripts": {
    "test""echo \"Error: no test specified\" && exit 1"
    "build""tsc",
    "format""prettier --write \"src/**/*.ts\" \"src/**/*.js\"",
    "lint""tslint -p tsconfig.json"
  },
  "repository": {
    "type""git",
    "url""git+https://github.com/simuty/toolkit.git"
  },
  "keywords": [
    "工具包",
    "toolkit"
  ],
  "author""simuty",
  "license""ISC",
  "bugs": {
    "url""https://github.com/simuty/toolkit/issues"
  },
  "homepage""https://github.com/simuty/toolkit#readme",
  "devDependencies": {
    "@types/jest""^24.9.1",
    "jest""^24.9.0",
    "prettier""^1.18.2",
    "ts-jest""^24.3.0",
    "tslint""^5.18.0",
    "tslint-config-prettier""^1.18.0",
    "typescript""^3.5.2"
  },
  "files": [
    "lib/**/*"
  ]
}

Now you can run npm run lint and npm run format in the console:

npm run lint
npm run format

 

Don’t include more than you need in your package!

In our .gitignore file, we added /lib since we don’t want the build-files in our git repository. The opposite goes for a published package. We don’t want the source code, only the build-files!
This can be solved in two ways. One way is to blacklist files/folders in a .npmignore file. Should have looked something like this in our case:

src
tsconfig.json
tslint.json
.prettierrc

However, blacklisting files is not a good practice. Every new file/folder added to the root, needs to be added to the .npmignore file as well! Instead, you should whitelist the files /folders you want to publish. This can be done by adding the files property in package.json:

"files": ["lib/**/*"]

That’s it! Easy 😃 Only the lib folder will be included in the published package! (README.md and package.json are added by default).
For more information about whitelisting vs blacklisting in NPM packages see this post from the NPM blog. (Thank you Tibor Blénessy for the reference)

Setup Testing with Jest

An awesome package should include unit tests! Let’s add Jest: An awesome testing framework by Facebook.

 

Since we will be writing tests against our typescript source-files, we also need to add ts-jest and @types/jest. The test suite is only used during development so let’s add them as devDependencies

npm install --save-dev jest ts-jest @types/jest

Cool! Now we need to configure Jest. You can choose to write a jest section to package.json or to create a separate config file. We are going to add it in a separate file, so it will not be included when we publish the package.
Create a new file in the root and name it jestconfig.json:

{
  "transform": {
    "^.+\\.(t|j)sx?$""ts-jest"
  },
  "testRegex""(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
  "moduleFileExtensions": ["ts""tsx""js""jsx""json""node"]
}

Remove the old test script in package.json and change it to:

"test""jest --config jestconfig.json",

The package.json should look something like this:

{
  "name""w-toolkit",
  "version""1.0.1",
  "description""Node 工具包",
  "main""lib/index.js",
  "types""lib/index.d.ts",
  "scripts": {
    "test""jest --config jestconfig.json",
    "build""tsc",
    "format""prettier --write \"src/**/*.ts\" \"src/**/*.js\"",
    "lint""tslint -p tsconfig.json"
  },
  "repository": {
    "type""git",
    "url""git+https://github.com/simuty/toolkit.git"
  },
  "keywords": [
    "工具包",
    "toolkit"
  ],
  "author""simuty",
  "license""ISC",
  "bugs": {
    "url""https://github.com/simuty/toolkit/issues"
  },
  "homepage""https://github.com/simuty/toolkit#readme",
  "devDependencies": {
    "@types/jest""^24.9.1",
    "jest""^24.9.0",
    "prettier""^1.18.2",
    "ts-jest""^24.3.0",
    "tslint""^5.18.0",
    "tslint-config-prettier""^1.18.0",
    "typescript""^3.5.2"
  },
  "files": [
    "lib/**/*"
  ]
}

Write a basic test

It’s time to write our first test! 😃
In the src folder, add a new folder called __tests__ and inside, add a new file with a name you like, but it has to end with test.ts, for example Greeter.test.ts

import { Greeter } from '../index';
test('My Greeter', () => {
  expect(Greeter('Carl')).toBe('Hello Carl');
});

Ok, so the only thing we are doing here is to verify that Our method Greeter will return Hello Carl if the input is Carl.
Now, Try to run

npm test

 

Cool it works! As you can see we passed one test.

Use the magic scripts in NPM

For an awesome package, we should of course automate as much as possible. We’re about to dig into some scripts in npm: prepare, prepublishOnly, preversion, version and postversion

  1. prepare will run both BEFORE the package is packed and published, and on local npm install. Perfect for running building the code. Add this script to package.json "prepare" : "npm run build"

  2. prepublishOnly will run BEFORE prepare and ONLY on npm publish. Here we will run our test and lint to make sure we don’t publish bad code: "prepublishOnly" : "npm test &amp;&amp; npm run lint"

  3. preversion will run before bumping a new package version. To be extra sure that we’re not bumping a version with bad code, why not run lint here as well? 😃 "preversion" : "npm run lint"

  4. Version will 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. One idea is to run the formatter here and so no ugly code will pass into the new version: "version" : "npm run format &amp;&amp; git add -A src"

  5. postversion will run after the commit has been made. A perfect place for pushing the commit as well as the tag. "postversion" : "git push &amp;&amp; git push --tags"

This is how my scripts section in package.json looks like:

"scripts": {
   "test""jest --config jestconfig.json",
   "build""tsc",
   "format""prettier --write \"src/**/*.ts\" \"src/**/*.js\"",
   "lint""tslint -p tsconfig.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"
}

Finishing up package.json

It’s finally time to finish up our awesome package! First, we need to make some changes to our package.json again:

{
  "name""w-toolkit",
  "version""1.0.1",
  "description""Node 工具包",
  "main""lib/index.js",
  "types""lib/index.d.ts",
  "scripts": {
    "test""jest --config jestconfig.json",
    "build""tsc",
    "format""prettier --write \"src/**/*.ts\" \"src/**/*.js\"",
    "lint""tslint -p tsconfig.json",
    "prepare""npm run build",
    "prepublishOnly""npm run 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/simuty/toolkit.git"
  },
  "keywords": [
    "工具包",
    "toolkit"
  ],
  "author""simuty",
  "license""ISC",
  "bugs": {
    "url""https://github.com/simuty/toolkit/issues"
  },
  "homepage""https://github.com/simuty/toolkit#readme",
  "devDependencies": {
    "@types/jest""^24.9.1",
    "jest""^24.9.0",
    "prettier""^1.18.2",
    "ts-jest""^24.3.0",
    "tslint""^5.18.0",
    "tslint-config-prettier""^1.18.0",
    "typescript""^3.5.2"
  },
  "files": [
    "lib/**/*"
  ]
}

Se here we are adding a nice description, an author and some relevant keywords. The key main is important here since it will tell npm where it can import the modules from.
The key types will tell Typescript and Code-editors where we can find the type definitions!

Commit and push your code to git

Time to push all your work to your remote repository! If you haven’t committed your latest code already, now it is the time to do it. 😉

git add -A && git commit -m "Setup Package"
git push

Publish you package to NPM!

In order to publish your package, you need to create an NPM account.
If you don’t have an account you can do so on https://www.npmjs.com/signupor run the command: npm adduser

If you already have an account, run npm login to login to you NPM account.

Logging in to my existing NPM account
Alright! Now run publish.

As you can see the package will first be built by the prepare script, then test and lint will run by the prepublishOnly script before the package is published.

 

View your package

Now browse your package on npmjs. The url is https://npmjs.com/package/ in my case it is
https://npmjs.com/package/my-awesome-greeter

 

Nice! We got a package 😎 📦 looking good so far!

Bumping a new version

Let’s bump a new patch version of the package:

npm version patch

Our preversion, version, and postversion will run, create a new tag in git and push it to our remote repository. Now publish again:

npm publish

And now you have a new version

 

What's next?

For the scope of this tutorial, I would like to stop here for now on something I would call a “minimum setup for an NPM Package”. However, when your package grows I would recommend:

  1. Setup automated build with Travis

  2. Analyze code-coverage with Codecov

  3. Add badges to your readme with Shields. Everyone loves badges 😎

But let’s leave that for another tutorial.

Good luck building your awesome package! 😃

 


全部评论: 0

    我有话说:

    Node包管理NPM(二)

    NPM是什么? [NPM官网](https://docs.npmjs.com/)给出解释如下: ``` Use npm to install, share, and distribute

    nvm常见配置问题

      本文涉及使用nvm时候 常见的三个问题 zsh: command not found: npm curl: (7) Failed to connect to raw

    【简单】Docker - 实战TLS加密通讯

    快速配置一个最简单的docker TLS加密通讯

    谷歌AI创造了AI,比人类编写的更加强大

    AI也能自行繁衍了。根据外媒报道。谷歌的AI系统AutoML创造了一个AI——NASNet,测试后发现,NASNet的表现已经比人类工程师撰写的AI更为强大。ML是机器学习(machine

    Apache Ant 1.10.10 发布

    Apache Ant 1.10.10 已发布。Apache Ant 是一个将软件编译、测试、部署等步骤联系在一起加以自动化的一个工具,大多用于 Java 环境中的软件开发。 Apache Ant

    【开源资讯】Ant Design 4.0.1 发布,企业级 UI 设计语言

    Ant Design 是阿里开源的一套企业级的 UI 设计语言和 React 实现,使用 TypeScript 构建。

    Gradle 6.8 发布,禁用 TLS v1.0 和 v1.1 协议

    Gradle 6.8 已经发布。Gradle 是一个基于 Apache Ant 和 Apache Maven 概念的项目自动化构建工具

    【开源资讯】Ant Design 4.8.5 发布,修复组件不能渲染等问题

    Ant Design 4.8.5 发布了。Ant Design 是一套企业级的 UI 设计语言和 React 实现,使用 TypeScript 构建,提供完整的类型定义文件,自带提炼自企业级中后台

    版本控制介绍

    原文:An introduction to version control 如果你对版本控制很有兴趣,但还没有真正使用,最可能的原因是版本控制让人困惑。如果你是新手的话,了解一下版本控制的基本概念

    京东技术:如何实现百万TPS?详解JMQ4的存储设计

    JMQ是京东中间件团队自研的消息中间件,诞生于2014年,服务京东近万个应用,2018年11.11大促期间的峰值流量超过5000亿条消息

    Node&RabbitMQ系列二 延迟|死信队列

      前提 目前项目中采用ts+eggjs结合的方式,针对定时任务,采用schedule,随着业务的增多,觉得缺点啥,可能就是缺消息队列吧。上一篇文章,针对rabbitmq的基本语法进行了

    《CSS 揭秘》译者,百姓网前端架构 TL 带来更多干货,等你来撩~~~

    春节小长假已经过完了,《IT实战联盟》 也开始准备更新更多优质的实战资源帮助大家解决工作中的实际问题。

    TensorFlow 2.3.2 发布,端到端开源机器学习平台

      TensorFlow 2.3.2 发布了,主要更新内容包括: 修复了对 Eigen 代码中的统一内存访问(CVE-2020-26266) 修复了由于tf.raw_ops

    「转载」分布式事务方案 - SAGA模式

    性能与架构:杜亦舒原文:https://mp.weixin.qq.com/s/An6QbAOw6jhWxg7GDT56Ug 本文目的是讲清楚 SAGA 这种分布式事务解决方案的实现思路,不包括具体

    Wine 6.0-RC5 发布,修复 21 个 bug

    Wine 6.0-RC5 已经发布。Wine(Wine Is Not an Emulator)是一个能够在多种兼容 POSIX 接口的操作系统(诸如 Linux、macOS 与 BSD 等

    ImageIO Unsupported Image Type

    : Unsupported Image Type at com.sun...

    MongoDB更新(五)

    如果数据库中尚未有数据, 准备测试数据db.test1.insertMany([    {"name": "zhangsan", "age": 19, "score": [90