Guide for create first NPM package with typescript. Tutorial from scratch to productoin ready NPM package.
- Visual Studio Code
- Git bash
- Command Line
- Prettier Extension for VSCode
- NPM
- Typscript
- Eslint
Create folder/directory at accessible location
mkdir age-calculator
cd age-calculator
Initialize NPM project. Provides the required information like package name, author etc
npm init
It create package.json file for the project with given instruction and data
pacakge.json
{
"name": "@dipaktelangre/age-calculator",
"version": "1.0.0",
"description": "Calculate age from date of birth to today",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Dipak Telangre",
"license": "ISC"
}
Lets initialize git in project to maintatin version from start.
Download and Install Git
Initialize current directory as git directory
Stage files to commit
git add . Stage all files
Commit staged files
git commit -m "First init" Commit staged files with message
Create .gitignore file to ignore files and folders from git
.gitignore
# build files
dist
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next
Check git status
git branch branch_name create new branch
git checkout branch_name Checkout to the branch
git log Commit logs
Create rpository for current porject over github. If you don't have account please sign up to github and create repository named age-calculator.
- Add git remote to github repository
git remote add origin https://github.com/dipaktelangre/age-calculator.git
git remote -v check remote
push changes to git remote
set upstream branch and push change to remote
Install typescript as dev dependence and save changes to package.json
npm i typescript -D short command for same
Create folder src for code files
Add index.ts file to src folder
index.ts
const test = () => {
console.log("Hello from typescript");
};
test();Add tsconfig.json file at root level where package.json file exist.
tsconfig.json
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"declaration": true,
"outDir": "./dist",
"strict": true
},
"include": ["src"],
"exclude": ["node_modules"]
}
Add build task in script section of package.json to compile typescript
package.json
"scripts": {
"build": "tsc",
"test": "echo \"Error: no test specified\" && exit 1"
},Once build task added to package.json, lets run build to compile the typescript files in js.
npm run build This will create dist folder and index.js, index.d.ts files under dist folder.
index.d.ts file is for type defination for typings
Test js created by node dist\index.js it will console the message from test method
Linting tool to find and fix problems in your JavaScript code
An opinionated code formatter. Format your dirty code as per standerd
- Insall eslint and its usefull plugin
npm install --save-dev eslint tslint eslint-config-prettier eslint-plugin-import eslint-plugin-unused-imports @typescript-eslint/eslint-plugin @typescript-eslint/eslint-plugin-tslint
- Install prettier and its plugin
npm install --save-dev prettier prettier-eslint eslint-config-prettier eslint-plugin-prettier
- Create
.eslintrc.jsfile for eslint configuration
.eslintrc.js
module.exports = {
env: {
browser: true,
node: true,
},
parser: "@typescript-eslint/parser",
extends: [
"plugin:@typescript-eslint/recommended",
"prettier/@typescript-eslint",
"plugin:prettier/recommended",
],
parserOptions: {
ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
project: "tsconfig.json",
sourceType: "module", // Allows for the use of imports
},
plugins: [
"@typescript-eslint",
"@typescript-eslint/tslint",
"import",
"unused-imports",
],
rules: {
"import/no-unresolved": "error",
"import/order": "error",
"@typescript-eslint/no-unused-vars": "off",
"unused-imports/no-unused-imports-ts": "error",
"unused-imports/no-unused-vars-ts": "error",
"no-console": ["warn", { allow: ["warn", "error"] }],
eqeqeq: ["error", "always"],
"no-else-return": "error",
},
settings: {
"import/resolver": {
node: {
extensions: [".js", ".jsx", ".ts", ".tsx"],
moduleDirectory: ["node_modules", "src/"],
},
},
},
};- Create
.eslintignorefile to ignore file from linting
.eslintignore
*.eslintrc.js
**/**/*.eslintrc.js
dist
- Create
.prettierrcfile for Prettier configuration
.prettierrc
{
"singleQuote": false,
"printWidth": 120,
"semi": true,
"trailingComma": "es5",
"endOfLine": "auto"
}
- Create
.prettierignorefile to ignore files from prettier
.prettierignore
# Ignore artifacts:
dist
- Add task in script section for linting and pritter
package.json
"scripts": {
"build": "tsc",
"lint:js": "eslint \"*/**/*.{js,ts}\" ",
"lint:js:fix": "eslint \"./src/**/*.{js,ts}\" --fix",
"lint": "npm run lint:js",
"prettier": "prettier --check \"*/**/*.{js,ts,json}\"",
"prettier:fix": "prettier --write \"*/**/*.{js,ts,json}\"",
"test": "echo \"Error: no test specified\" && exit 1"
},
- Run
npm run lintto lint the js & ts files
Jest is a delightful JavaScript Testing Framework with a focus on simplicity.
npm i -D jest ts-jest @types/jest Install jest,ts-jest
- Create
jestconfig.json
jestconfig.json
{
"transform": {
"^.+\\.(t|j)sx?$": "ts-jest"
},
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"]
}
- Create
__tests__folder at root level - Create
fist.test.tsfile in__tests__folder for test case
fist.test.ts
import { sum } from "../src/index";
test("adds 1 + 2 to equal 3", () => {
expect(sum(1, 2)).toBe(3);
});
- Change
index.tsfile code, lets add sum methode for testing
index.ts
export const sum = (a: any, b: any) => {
return a + b;
};
- Modify
testtask in script section ofpackage.json
"scripts": {
"build": "tsc",
"lint:js": "eslint \"*/**/*.{js,ts}\" ",
"lint:js:fix": "eslint \"./src/**/*.{js,ts}\" --fix",
"lint": "npm run lint:js",
"prettier": "prettier --check \"*/**/*.{js,ts,json}\"",
"prettier:fix": "prettier --write \"*/**/*.{js,ts,json}\"",
"test": "jest --config jestconfig.json",
"test:tdd": "jest --watch --config jestconfig.json"
}
- Run tests by
npm run test
All set for testing !!
Note if you run
npm run lintyou will errorerror Parsing error: "parserOptions.project" has been set for @typescript-eslint/parser.as__test__not part of the project
Create tsconfig.eslint.json file at root level
tsconfig.eslint.json
{
"extends": "./tsconfig.json",
"include": ["__tests__", "src"]
}
Change .eslintrc.js file parcerOption section
.eslintrc.js
module.exports = {
...
parserOptions: {
project: "tsconfig.eslint.json",
...
},
...
};
- Run
npm run linterror should gone now
Create required code for age calculator.
- Update
index.ts
import moment from "moment";
import { Age } from "./model";
export class AgeCalculator {
/**
* Get age in years, month, days format from given date of birth
* @param dob date of birth
* @returns age in {years, months, days}
*/
static getAge(dob: Date): Age {
if (!(dob instanceof Date)) throw new Error("Invalid date");
if (moment(dob.toISOString()).isAfter(moment())) throw new Error("Future date not allowed");
return AgeCalculator.dateDifference(dob, new Date());
}
private static dateDifference(from: Date, to: Date): Age {
if (!(from instanceof Date) || !(to instanceof Date)) throw new Error("Invalid date");
let fromDate = moment(from.toISOString());
let toDate = moment(to);
let years = toDate.diff(fromDate, "year");
fromDate.add(years, "years");
let months = toDate.diff(fromDate, "months");
fromDate.add(months, "months");
let days = toDate.diff(fromDate, "days");
return {
years: years,
months: months,
days: days,
};
}
}
- Create
model.tsfile insrcfolder
export interface Age {
years: number;
months: number;
days: number;
}
- Create
age-calculator.test.tsfile in__tests__folder
import { AgeCalculator } from "../src/index";
test("my age should be greater than 29 years", () => {
const age = AgeCalculator.getAge(new Date("01-01-1990"));
expect(age.years).toBeGreaterThan(29);
});
test("future dob should throw error", () => {
expect(() => {
AgeCalculator.getAge(new Date("12-29-2050"));
}).toThrow();
expect(() => {
AgeCalculator.getAge(new Date("12-29-2050"));
}).toThrowError(Error);
});
test("future dob should throw expected error", () => {
expect(() => {
AgeCalculator.getAge(new Date("12-29-2050"));
}).toThrowError(Error("Future date not allowed"));
});
- Run
npm run test:tddfor devloping in test driven mode
Husky can prevent bad git commit, git push and more 🐶 woof!
npm install husky lint-staged
- Create
huskytask inpackage.jsonfile
package.json
"husky": {
"hooks": {
"pre-commit": "lint-staged && npm run build && npm run test"
}
},
"lint-staged": {
"*.{ts,js}": [
"eslint --fix"
],
"*.{ts,js,json}": [
"prettier --write"
]
}
- Commit changes and husky hooks should take care before commit happens
git add .
git commit -m "Add husky hooks
Lets add some more functionality to the Age calculator and some more unit test cases
index.ts
import moment from "moment";
import { UnitOfAge, Age } from "./model";
import { DateCalculator } from "./date-calculator";
export class AgeCalculator {
/**
* Get age in years, month, days format from given date of birth
* @param dob date of birth
* @returns age in {years, months, days}
*/
static getAge(dob: Date): Age {
if (!(dob instanceof Date)) throw new Error("Invalid date");
if (moment(dob.toISOString()).isAfter(moment())) throw new Error("Future date not allowed");
let dateCal = new DateCalculator();
return dateCal.dateDifference(dob, new Date());
}
/**
* Get age in given formate
* @param dob date of birth
* @param format unit of age i.g. years, months, weeks, days, hours etc
*/
static getAgeIn(dob: Date, format: UnitOfAge): number {
if (!(dob instanceof Date)) throw new Error("Invalid date");
if (moment(dob.toISOString()).isAfter(moment())) throw new Error("Future date not allowed");
let dateCal = new DateCalculator();
return dateCal.dateDifferenceIn(dob, new Date(), format);
}
}
model.ts
export interface Age {
years: number;
months: number;
days: number;
}
export type UnitOfAge =
| "year"
| "years"
| "y"
| "month"
| "months"
| "M"
| "week"
| "weeks"
| "w"
| "day"
| "days"
| "d"
| "hour"
| "hours"
| "h"
| "minute"
| "minutes"
| "m"
| "second"
| "seconds"
| "s"
| "millisecond"
| "milliseconds"
| "ms";
date-calculator.ts
import moment from "moment";
import { UnitOfAge, Age } from "./model";
export class DateCalculator {
dateDifference(from: Date, to: Date): Age {
if (!(from instanceof Date) || !(to instanceof Date)) throw new Error("Invalid date");
let fromDate = moment(from.toISOString());
let toDate = moment(to);
let years = toDate.diff(fromDate, "year");
fromDate.add(years, "years");
let months = toDate.diff(fromDate, "months");
fromDate.add(months, "months");
let days = toDate.diff(fromDate, "days");
return {
years: years,
months: months,
days: days,
};
}
dateDifferenceIn(from: Date, to: Date, format: UnitOfAge): number {
if (!(from instanceof Date) || !(to instanceof Date)) throw new Error("Invalid date");
let fromDate = moment(from.toISOString());
let toDate = moment(to);
return toDate.diff(fromDate, format);
}
}
age-calculator.test.ts
import { AgeCalculator } from "../src/index";
test("my age should be greater than 29 years", () => {
const age = AgeCalculator.getAge(new Date("01-01-1990"));
expect(age.years).toBeGreaterThan(29);
});
test("future dob should throw error", () => {
expect(() => {
AgeCalculator.getAge(new Date("12-29-2050"));
}).toThrow();
expect(() => {
AgeCalculator.getAge(new Date("12-29-2050"));
}).toThrowError(Error);
});
test("future dob should throw expected error", () => {
expect(() => {
AgeCalculator.getAge(new Date("12-29-2050"));
}).toThrowError(Error("Future date not allowed"));
});
test("should get age in years", () => {
let ageInYear = AgeCalculator.getAgeIn(new Date("01-01-2010"), "years");
expect(ageInYear).toBe(10);
});
date-calculator.test.ts
import { DateCalculator } from "../src/date-calculator";
describe("Date Difference Calculator", () => {
test("calculate date diffrence for year", () => {
let dateCal = new DateCalculator();
let diff = dateCal.dateDifference(new Date("01-01-2019"), new Date("01-01-2020"));
expect(diff.years).toBe(1);
});
test("calculate diff for single year ", () => {
let dateCal = new DateCalculator();
let diff = dateCal.dateDifference(new Date("01-01-2019"), new Date("12-31-2019"));
expect(diff.years).toBe(0);
expect(diff.months).toBe(11);
expect(diff.days).toBe(30);
});
test("calculate date diff in weeks", () => {
let dateCal = new DateCalculator();
let diff = dateCal.dateDifferenceIn(new Date("01-01-2019"), new Date("12-31-2019"), "weeks");
expect(diff).toBe(52);
});
test("calculate date diff in days", () => {
let dateCal = new DateCalculator();
let diff = dateCal.dateDifferenceIn(new Date("01-01-2019"), new Date("12-31-2019"), "days");
expect(diff).toBe(364);
});
test("calculate date diff in months", () => {
let dateCal = new DateCalculator();
let diff = dateCal.dateDifferenceIn(new Date("01-01-2019"), new Date("12-31-2019"), "month");
expect(diff).toBe(11);
});
});
Lets commit all the changes
git add .
git commit -m "Add functionality to get age in given format i.e weeks, days etc"
Husky task will run on all the staged changes. It will lint changes and fix lint issue as well as format code by prettier if any issue persist.
Check code files varaibles those not reassigned would be changed to cost
So all the formating under the hood !!!
Lets create NPM pacakge and publish it
- Update information in
package.jsonfile. Likekeywords,author,homepageetc
We don't want to publish source code files, we only want to ship build library files.
files whitelist the files to be include in package
pakage.json
{
"name": "@dipaktelangre/age-calculator",
"version": "1.0.0",
"description": "Calculate age from date of birth to today",
"keywords": [
"age calculator",
"age in weeks",
"calculate age",
"whats my age"
],
"homepage": "https://github.com/dipaktelangre/age-calculator",
"repository": {
"type": "git",
"url": "https://github.com/dipaktelangre/age-calculator"
},
"files": [
"dist/**/*"
],
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"lint:js": "eslint \"*/**/*.{js,ts}\" ",
"lint:js:fix": "eslint \"./src/**/*.{js,ts}\" --fix",
"lint": "npm run lint:js",
"prettier": "prettier --check \"*/**/*.{js,ts,json}\"",
"prettier:fix": "prettier --write \"*/**/*.{js,ts,json}\"",
"test": "jest --config jestconfig.json",
"test:tdd": "jest --watch --config jestconfig.json"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged && npm run build && npm run test"
}
},
"lint-staged": {
"*.{ts,js}": [
"eslint --fix"
],
"*.{ts,js,json}": [
"prettier --write"
]
},
"author": "Dipak Telangre",
"license": "ISC",
"devDependencies": {
"@types/jest": "^26.0.3",
"@typescript-eslint/eslint-plugin": "^3.5.0",
"@typescript-eslint/eslint-plugin-tslint": "^3.5.0",
"eslint": "^7.3.1",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-unused-imports": "^0.1.3",
"jest": "^26.1.0",
"prettier": "^2.0.5",
"prettier-eslint": "^11.0.0",
"ts-jest": "^26.1.1",
"tslint": "^6.1.2",
"typescript": "^3.9.6"
},
"dependencies": {
"husky": "^4.2.5",
"lint-staged": "^10.2.11",
"moment": "^2.27.0"
}
}
Package current directory into tgz package. it get information from package.json file
npm pack will create dipaktelangre-age-calculator-1.0.0.tgz package file which can be installed and published
Lets try to install package locally and test it before publish to NPM feed
Change to any another directory test package
cd test-age
npm i dipaktelangre-age-calculator-1.0.0.tgz Install package from file. file should in current dir or provide correct path to tgz file
Let test Age Calculator in Node CLI
node open node rpl
var {AgeCalculator} = require("@dipaktelangre/age-calculator") // undefined
AgeCalculator.getAge(new Date("01/01/1990")); //{ years: 30, months: 6, days: 3 }
AgeCalculator.getAgeIn(new Date("01/01/1990"), "years"); // 30
AgeCalculator.getAgeIn(new Date("01/01/1990"), "weeks"); // 1591
AgeCalculator.getAgeIn(new Date("01/01/1990"), "month"); //366
Create account/login to NPM feed https://www.npmjs.com/
Once your account on https://www.npmjs.com/ let to terminal and project directory
npm login to login npm feed
Create package from current direcotory and publish it to NPM feed
IF you get this error 402 Payment Required - PUT https://registry.npmjs.org/@dipaktelangre%2fage-calculator - You must sign up for private packages its because by default package it private and we need sign up for private packages to be hosted over npmjs.
Lets change package to public
Add private:false property in package.json file and try npm publish --access=public
Publish package as public
Congratulation !! you have your first package ready !!!
Lets installed your package from public and test it
npm i @dipaktelangre/age-calculator Test it until you satiesfy
Unpublish npm package
Lets remove our package for now
npm unpublish @dipaktelangre/age-calculator --force
Package without no documentation is not so usefull. Lets create documentation for the package.
-
Create
README.mdfile -
Add documentation for package
-
Commit changes
git add .
git commit -m "Add README.md"
- Publish package again last time
npm run build
npm login
npm version patch
npm publish --access=public
-------------------------- All the Best ------------------------