12
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

npmパッケージをTypeScriptで作成して自動デプロイで公開するまでの手順の詳細

Last updated at Posted at 2021-08-01

先日はじめてnpmパッケージの作成~公開までをやってみました。
私が最近TypeScriptで書くことが多いため、今回TypeScriptで作成しました。

今回はその手順を詳しく、次に作る方の参考になるように、手順を書いていきたいと思います。

作成にあたって使用した技術は、

  • TypeScript
  • Jest
  • GitHub Actions

なども含みますので、JestやGithub Actionsのコードについても示していきます。

作ったパッケージの概要

作ったのはこちらです。

これは、都道府県データを簡単に扱えるようにしたパッケージです。
使い方はREADMEにも書かれていますが、一部抜粋すると以下のようです。

import { findByCode, filterByArea, prefectureNames } from "jp-prefectures";

findByCode(13);
//=> {code: 13, name: "東京都", enName: "tokyo", area: "関東", capital: "新宿区"}

filterByArea("関東")
/*=>
[
  {code: 8, name: "茨城県", enName: "ibaraki", area: "関東", capital: "水戸市"},
  {code: 9, name: "栃木県", enName: "tochigi", area: "関東", capital: "宇都宮市"},
  ...,
  {code: 14, name: "神奈川県", enName: "kanagawa", area: "関東", capital: "横浜市"}
]
*/

prefectureNames()
//=> ["北海道", "青森県", ..., "沖縄県"]

上記のような関数で、都道府県情報が取得できるようになるというものです。

さて、パッケージの概要がわかったところで、作り方の解説に移ります。

事前準備 npmjs.comに登録

npmを公開するに当たっては、以下のサイトに登録しておく必要があります。

登録が完了したらターミナルから npm addusernpmjs.com に接続できるように設定します。

$ npm adduser
npm notice Log in on https://registry.npmjs.org/
Username: hatsu38
Password:
Email: (this IS public)

package.jsonを作成

npm init

を実行すると、対話形式でpackage.jsonファイルの作成を行います。

ちなみに公開にあたっては、package名に大文字は使えないので、ご注意ください。

こちらで作成を行ったあとも編集が可能です。以降の手順でも編集は行いますので、ひとまずデフォルトで進めて構いません。

TypeScript導入

npm install typescript

TypeScriptをインストールします。

インストールが完了すると、node_modulesフォルダが作成されます。そのフォルダは .gitignoreに追加しておきます。

.gitignoreファイル↓

node_modules/

tsconfig.jsonも作成しておきます。

tsc --init

作成されたtsconfig.jsonを一部修正します、以下のようになりました。

{
  "compilerOptions": {
    "target": "es2015",                                /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
    "module": "es2015",                           /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
    "declaration": true,                         /* Generates corresponding '.d.ts' file. */
    "sourceMap": true,                           /* Generates corresponding '.map' file. */
    "outDir": "./dist",                              /* Redirect output structure to the directory. */
    "rootDir": "./",                             /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
    "importHelpers": true,                       /* Import emit helpers from 'tslib'. */
    "strict": true,                                 /* Enable all strict type-checking options. */
    "moduleResolution": "node",                  /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
    "baseUrl": "./",                             /* Base directory to resolve non-absolute module names. */
    "rootDirs": ["./src/"],                              /* List of root folders whose combined content represents the structure of the project at runtime. */
    "typeRoots": ["node_modules/@types", "./src/types"],  /* List of folders to include type definitions from. */
    "allowSyntheticDefaultImports": true,        /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
    "esModuleInterop": true,                        /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
    "resolveJsonModule": true,
    "skipLibCheck": true,                           /* Skip type checking of declaration files. */
    "forceConsistentCasingInFileNames": true,       /* Disallow inconsistently-cased references to the same file. */
  },
  "include": ["src/**/*"],
  "exclude": [
    "node_modules",
    "dist",
    "test",
  ],
}

index.tsファイル作成

今回、srcフォルダにindex.tsファイルを作っていきます。

mkdir src && touch src/index.ts

src/index.ts の中身については、冒頭で紹介したリポジトリのコードの一部を利用することにします。

import prefs from "../data/prefectures.json";

interface Prefecture {
  code: number;
  name: string;
  enName: string;
  area: string;
  capital: string;
}

function findByName(value: string): Prefecture | undefined {
  return prefs.find((pref: Prefecture) => pref.name === value);
}

function findByCode(value: string | number): Prefecture | undefined {
  return prefs.find((pref: Prefecture) => pref.code === Number(value));
}

export {
  findByName,
  findByCode,
};

data/prefectures.jsonの中身は以下で折りたたんで記載します。

data/prefectures.jsonの中身
[
  { "code": 1, "name": "北海道", "enName": "hokkaido", "area": "北海道", "capital": "札幌市" },
  { "code": 2, "name": "青森県", "enName": "aomori", "area": "東北", "capital": "青森市" },
  { "code": 3, "name": "岩手県", "enName": "iwate", "area": "東北", "capital": "盛岡市" },
  { "code": 4, "name": "宮城県", "enName": "miyagi", "area": "東北", "capital": "仙台市" },
  { "code": 5, "name": "秋田県", "enName": "akita", "area": "東北", "capital": "秋田市" },
  { "code": 6, "name": "山形県", "enName": "yamagata", "area": "東北", "capital": "山形市" },
  { "code": 7, "name": "福島県", "enName": "fukushima", "area": "東北", "capital": "福島市" },
  { "code": 8, "name": "茨城県", "enName": "ibaraki", "area": "関東", "capital": "水戸市" },
  { "code": 9, "name": "栃木県", "enName": "tochigi", "area": "関東", "capital": "宇都宮市" },
  { "code": 10, "name": "群馬県", "enName": "gunma", "area": "関東", "capital": "前橋市" },
  { "code": 11, "name": "埼玉県", "enName": "saitama", "area": "関東", "capital": "さいたま市" },
  { "code": 12, "name": "千葉県", "enName": "chiba", "area": "関東", "capital": "千葉市" },
  { "code": 13, "name": "東京都", "enName": "tokyo", "area": "関東", "capital": "新宿区" },
  { "code": 14, "name": "神奈川県", "enName": "kanagawa", "area": "関東", "capital": "横浜市" },
  { "code": 15, "name": "新潟県", "enName": "niigata", "area": "中部", "capital": "新潟市" },
  { "code": 16, "name": "富山県", "enName": "toyama", "area": "中部", "capital": "富山市" },
  { "code": 17, "name": "石川県", "enName": "ishikawa", "area": "中部", "capital": "金沢市" },
  { "code": 18, "name": "福井県", "enName": "fukui", "area": "中部", "capital": "福井市" },
  { "code": 19, "name": "山梨県", "enName": "yamanashi", "area": "中部", "capital": "甲府市" },
  { "code": 20, "name": "長野県", "enName": "nagano", "area": "中部", "capital": "長野市" },
  { "code": 21, "name": "岐阜県", "enName": "gifu", "area": "中部", "capital": "岐阜市" },
  { "code": 22, "name": "静岡県", "enName": "shizuoka", "area": "中部", "capital": "静岡市" },
  { "code": 23, "name": "愛知県", "enName": "ehime", "area": "中部", "capital": "名古屋市" },
  { "code": 24, "name": "三重県", "enName": "mie", "area": "関西", "capital": "津市" },
  { "code": 25, "name": "滋賀県", "enName": "shiga", "area": "関西", "capital": "大津市" },
  { "code": 26, "name": "京都府", "enName": "kyoto", "area": "関西", "capital": "京都市" },
  { "code": 27, "name": "大阪府", "enName": "osaka", "area": "関西", "capital": "大阪市" },
  { "code": 28, "name": "兵庫県", "enName": "hyogo", "area": "関西", "capital": "神戸市" },
  { "code": 29, "name": "奈良県", "enName": "nara", "area": "関西", "capital": "奈良市" },
  { "code": 30, "name": "和歌山県", "enName": "wakayama", "area": "関西", "capital": "和歌山市" },
  { "code": 31, "name": "鳥取県", "enName": "tottori", "area": "中国", "capital": "鳥取市" },
  { "code": 32, "name": "島根県", "enName": "shimane", "area": "中国", "capital": "松江市" },
  { "code": 33, "name": "岡山県", "enName": "okayama", "area": "中国", "capital": "岡山市" },
  { "code": 34, "name": "広島県", "enName": "hiroshima", "area": "中国", "capital": "広島市" },
  { "code": 35, "name": "山口県", "enName": "yamaguchi", "area": "中国", "capital": "山口市" },
  { "code": 36, "name": "徳島県", "enName": "tokushima", "area": "四国", "capital": "徳島市" },
  { "code": 37, "name": "香川県", "enName": "kagawa", "area": "四国", "capital": "高松市" },
  { "code": 38, "name": "愛媛県", "enName": "ehime", "area": "四国", "capital": "松山市" },
  { "code": 39, "name": "高知県", "enName": "kochi", "area": "四国", "capital": "高知市" },
  { "code": 40, "name": "福岡県", "enName": "fukuoka", "area": "九州", "capital": "福岡市" },
  { "code": 41, "name": "佐賀県", "enName": "saga", "area": "九州", "capital": "佐賀市" },
  { "code": 42, "name": "長崎県", "enName": "nagasaki", "area": "九州", "capital": "長崎市" },
  { "code": 43, "name": "熊本県", "enName": "kumamoto", "area": "九州", "capital": "熊本市" },
  { "code": 44, "name": "大分県", "enName": "oita", "area": "九州", "capital": "大分市" },
  { "code": 45, "name": "宮崎県", "enName": "miyazaki", "area": "九州", "capital": "宮崎市" },
  { "code": 46, "name": "鹿児島県", "enName": "kagoshima", "area": "九州", "capital": "鹿児島市" },
  { "code": 47, "name": "沖縄県", "enName": "okinawa", "area": "九州", "capital": "那覇市" }
]

Lintの導入

npm i --save-dev eslint prettier eslint-plugin-prettier @typescript-eslint/eslint-plugin@latest @typescript-eslint/parser eslint-config-prettier ts-node

開発環境でしか必要ないパッケージのインストールなので、--save-devオプションを付けておきます。

lintに必要なパッケージのインストールを行ったあと、 .eslintrc.json を作成します。

{
  "env": {
    "node": true,
    "jest": true,
    "browser": true
  },
  "parser": "@typescript-eslint/parser",
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended",
    "prettier"
  ],
  "ignorePatterns": ["dist/**"],
  "rules": {
    // 0は無効、1は警告、2はエラー
    "indent": ["error", 2,
      {"SwitchCase": 1}
    ], // インデントはSpaceを2
    "prefer-const": "error", // 再代入を行わない変数はconstを利用
    "quotes": ["error", "double"], // ダブルクオーテーションで文字列を囲う
    "semi": ["error", "always"], // セミコロンを必須にする
    "no-var": "error", // var禁止
    "no-unused-vars": "error", // 使用していない変数を削除
    "no-debugger": "error", // デバッガーは残さない
    "@typescript-eslint/no-explicit-any": "off"
  }
}

Linの設定が完了したら、package.jsonを編集して lintを走らせるScriptも設定しておきます。

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "lint": "eslint", // 追加
    "lint:fix": "eslint --fix"  // 追加
  },

上記をpackage.jsonに追加します。

すると以下のコマンドでLintのチェック、Lintによる修正を行ってくれます。

$ npm run lint .

> practice@1.0.0 lint
> eslint "."


$ npm run lint:fix .

> practice@1.0.0 lint
> eslint "."

Jestによるテストの導入

Jestの導入

テストはJestを利用します。

以下のコマンドでインストールを行います。

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

インストール後、以下のコマンドでjest.config.tsを作成します。

$ npx jest --init

The following questions will help Jest to create a suitable configuration for your project

✔ Would you like to use Jest when running "test" script in "package.json"? … yes
✔ Would you like to use Typescript for the configuration file? … yes
✔ Choose the test environment that will be used for testing › node
✔ Do you want Jest to add coverage reports? … yes
✔ Which provider should be used to instrument code for coverage? › babel
✔ Automatically clear mock calls and instances between every test? … yes

こちらも対話形式でjestの設定ファイル (jest.config.ts)が作成されます。

作成された jest.config.tspreset"ts-jest"に変更します。

- // preset: undefined,
+ preset: "ts-jest",

また、上記の対話でpackage.jsonのscriptsのtestコマンドが変わっているかもですが、以下のようになっていたら、大丈夫です。

  "scripts": {
    "test": "jest",
    "lint": "eslint",
    "lint:fix": "eslint --fix"
  },

これで npm run jestを実行すると、jestによるテストが走るようになります。

テスト作成

テストファイルは testフォルダに書くことにします。

mkdir -p test/src && touch test/src/index.test.ts

test/src/index.test.tsの中身は以下のようにしました。
なおjestの書き方などについては今回扱いません。

import data from "../../data/prefectures.json";
import {
  findByName,
  findByCode,
} from "../../src/index";

test("findByName", () => {
  expect(findByName("北海道")).toBe(data[0]);
  expect(findByName("hoge")).toBe(undefined);
});

test("findByCode", () => {
  expect(findByCode("1")).toBe(data[0]);
  expect(findByCode("01")).toBe(data[0]);
  expect(findByCode(1)).toBe(data[0]);
  expect(findByCode(0)).toBe(undefined);
});

これでテストの作成も完了です。
実際に走らせ見ると、以下のようになりました。

スクリーンショット 2021-08-01 14.58.22.png

GitHub ActionsでLintとTestのチェック

GitHub Actionsで先程作ったテストとLintのチェックをプルリクエストが作られたタイミングで走るようにしておきます。

mkdir -p .github/workflows && touch .github/workflows/ci.yml

ci.ymlの中身は以下のようにします。
以下のGitHubのDocsを参考にしていますので、解説は飛ばします。

name: CI

on:
  pull_request:
  push:
    branches:
      - master

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [10.x, 12.x, 14.x, 15.x] // 複数のnodeのバージョンでICを実行する

    steps:
      - uses: actions/checkout@v2

      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v2
        with:
          node-version: ${{ matrix.node-version }}

      - uses: actions/cache@v2
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node${{ matrix.node-version }}-${{ hashFiles('./package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node${{ matrix.node-version }}-${{ hashFiles('./package-lock.json') }}

      - name: Install
        run: npm ci --prefer-offline --no-audit --no-optional --ignore-scripts
        env:
          CI: true

      - name: Build
        run: npm run build --if-present

      - name: Test
        run: npm run test

      - name: Lint
        run: npm run lint .

プルリクエストを作ってCIがうまく走ると以下のようになります。

スクリーンショット 2021-07-31 1.12.42.png

いよいよ公開

npmパッケージの公開は

npm publish

で可能です。が、公開する前に、

  • Lintが通っているか
  • テストが通るか

という確認を行いたいので、その設定を行っておきます。

まずは package.jsonの変更を行います。

公開前にLintとTestを実行する

npm publish コマンドのライフサイクルがあります。

npm publish
・prepublishOnly
・prepack
・prepare
・postpack
・publish
・postpublish
prepare will not run during --dry-run

です。publish(公開)される前に実行をしたいので、今回は

  • prepublishOnlyのタイミングで、 npm run test && npm run lint
  • prepareのタイミングで、 npm run build

を実行します。

よってscriptsは以下のように変更しました。buildのscriptも追加しています。

  "scripts": {
    "build": "tsc --project tsconfig.json",
    "test": "jest",
    "lint": "eslint",
    "lint:fix": "eslint --fix",
    "prepublishOnly": "npm run test && npm run lint",
    "prepare": "npm run build"
  },

build結果に応じた修正

Entrypointの修正

buildを行うと、tsconfig.json"outDir": "./dist",と書いてあるとおり、distフォルダにbuildされたファイルが出力されます。

最初に呼ばれるファイルはBuildされたファイルのindex.jsにしたいので、
package.jsonのmainを変更します。

"main": "./dist/src/index.js",

型定義ファイルの指定

また型定義ファイルも指定してあげます。
tsconfig.json"declaration": true,と書いてあれば、Build時に型定義ファイルもdistフォルダに出力されます。
その出力されたファイルを型定義ファイルとして用いることをpackage.jsonで示します。

"types": "./dist/src/index.d.ts",

不要なファイルを公開しない指定

実際にこのパッケージをインストールして扱う際には、出力されたdistのフォルダだけで充分なので、それ以外のファイルは公開されないように指定します。指定方法は、package.jsonに以下を追加します。

  "files": [ "dist/" ],

ちなみにREADMEpackage.jsonファイルは上記のように指定しても公開されます。
その他にも明示しても含まれるファイル、明示しなくても含まれないファイルは存在しますので、気になる方は以下のドキュメントを参考にしてみてください。

またgitで管理する必要のないファイルが増えたので、併せて.gitignoreの編集も行っておきましょう。.gitignoreは以下のようになっています。

node_modules/
coverage/
dist/

ここまでできたらほぼ完成

ここまでできたら完成です。

いよいよ npm publishを実行してみましょう!

また公開にあたって、以下のようなコマンドでパッケージのバージョンを自動で上げることもできます。

npm version major
npm version minor
npm version patch

また非公開にしたい場合は以下の unpublishコマンドで可能です。

npm unpublish パッケージ名@バージョン

公開したら、実際に自分のプロジェクトにインストールして使ってみてください!

おまけ 自動デプロイ

GithubのRelease機能を使い、このリリースが作成されたら自動でnpm publishを行うように設定していきます。GithubのRelease機能↓

Deployを行うGithub Actionsはdeploy.ymlに作成しておきます。

touch .github/workflows/deploy.yml 

deploy.ymlの中身は以下のようなものです。

name: Deploy

on:
  release:
    types: [created]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Node 14 setup
        uses: actions/setup-node@v2
        with:
          node-version: '14'
          registry-url: 'https://registry.npmjs.org'

      - name: Install
        run: npm install

      - name: Test
        run: npm run test

      - name: Build
        run: npm run build --if-present

      - name: Publish
        run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # GitHub上にSecretKeyを設定

一番下の行にある、NPM_TOKENはGitHub上で設定します。

Actions_secrets.png

Actions_secrets.png

Tokenに入れる値は、.npmrcファイルにあるauthToken=以降の値です。

$ cat .npmrc
//registry.npmjs.org/:_authToken=hogehoge

こちらの設定ができたら、自動デプロイが可能になっています。

ターミナルで、以下の手順でリリースを作成可能です。リリースを作成すると、自動デプロイが走ります。

1.npmのバージョンを更新

npm version (patch | mainer | major)

2.TagをPush

git push origin v1.0.5

3.リリース

$ gh release create  v1.0.5
? Title (optional)  v1.0.5
? Release notes Write my own
? Is this a prerelease? No
? Submit? Publish release

スクリーンショット 2021-08-02 9.17.32.png

上の画像のようになっていたらnpmに公開もできています!

終わりに

これでnpmパッケージ公開の流れについてはおしまいです。
TypeScriptで実行、Jestの導入、Github Actionsの実行など公開に必須ではないものの、実際に公開するにあたっては欲しい機能などと思います。
npmパッケージを作る際には、ぜひ参考にしてみてください。

もし気に入ったらスターなどで応援してくれると喜びます。それでは。

12
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?