Skip to content

Instantly share code, notes, and snippets.

@dipaktelangre
Last active July 4, 2020 07:01
Show Gist options
  • Select an option

  • Save dipaktelangre/2ad8b57eca97165d7f14935ed23dfcd5 to your computer and use it in GitHub Desktop.

Select an option

Save dipaktelangre/2ad8b57eca97165d7f14935ed23dfcd5 to your computer and use it in GitHub Desktop.

Guide for create first NPM package with typescript. Tutorial from scratch to productoin ready NPM package.

Tools

  • Visual Studio Code
  • Git bash
  • Command Line
  • Prettier Extension for VSCode
  • NPM
  • Typscript
  • Eslint

Creating NPM Package project

Create folder/directory at accessible location

mkdir age-calculator

cd age-calculator

npm init

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"
}

Add git to project

Lets initialize git in project to maintatin version from start.

Download and Install Git

git init

Initialize current directory as git directory

git add

Stage files to commit

git add . Stage all files

git commit

Commit staged files git commit -m "First init" Commit staged files with message

.gitignore

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

git status

Check git status

git branch branch_name create new branch
git checkout branch_name Checkout to the branch
git log Commit logs

Github repo

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

git push

push changes to git remote

git push --set-upstream origin master

set upstream branch and push change to remote

Add Typescript

npm install typescript --save-dev

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"
  },

npm run build

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

Add ESLint & Prettier

ESLint

Linting tool to find and fix problems in your JavaScript code

Prettier

An opinionated code formatter. Format your dirty code as per standerd

Install dependency

  • 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.js file 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 .eslintignore file to ignore file from linting

.eslintignore

*.eslintrc.js
**/**/*.eslintrc.js
dist
  • Create .prettierrc file for Prettier configuration

.prettierrc

{
  "singleQuote": false,
  "printWidth": 120,
  "semi": true,
  "trailingComma": "es5",
  "endOfLine": "auto"
}
  • Create .prettierignore file 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 lint to lint the js & ts files

Add Jest Unit Testing

Jest is a delightful JavaScript Testing Framework with a focus on simplicity.

Install Dependencies

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.ts file 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.ts file code, lets add sum methode for testing

index.ts

export const sum = (a: any, b: any) => {
  return a + b;
};

  • Modify test task in script section of 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": "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 lint you will error error 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 lint error should gone now

Age Calculator

Create required code for age calculator.

Dependencies

npm install moment --save

  • 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.ts file in src folder
export interface Age {
  years: number;
  months: number;
  days: number;
}

  • Create age-calculator.test.ts file 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:tdd for devloping in test driven mode

Husky Git Hooks

Husky can prevent bad git commit, git push and more 🐶 woof!

Dependencies

npm install husky lint-staged

  • Create husky task in package.json file

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

Complete Age calculator

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 !!!

NPM Package

Lets create NPM pacakge and publish it

  • Update information in package.json file. Like keywords, author, homepage etc

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"
  }
}

npm pack

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

node

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

Publish to NPM feed

Create account/login to NPM feed https://www.npmjs.com/

npm login

Once your account on https://www.npmjs.com/ let to terminal and project directory

npm login to login npm feed

npm publish

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

npm publish --access=public

Publish package as public

Congratulation !! you have your first package ready !!!

Test

Lets installed your package from public and test it

npm i @dipaktelangre/age-calculator Test it until you satiesfy

npm unpublish

Unpublish npm package

Lets remove our package for now

npm unpublish @dipaktelangre/age-calculator --force

Documentation README.md

Package without no documentation is not so usefull. Lets create documentation for the package.

  • Create README.md file

  • 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 ------------------------

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment