先日はじめて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 adduser
で npmjs.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.ts
のpreset
を"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);
});
これでテストの作成も完了です。
実際に走らせ見ると、以下のようになりました。
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がうまく走ると以下のようになります。
いよいよ公開
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/" ],
ちなみにREADME
やpackage.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上で設定します。
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
上の画像のようになっていたらnpmに公開もできています!
終わりに
これでnpmパッケージ公開の流れについてはおしまいです。
TypeScriptで実行、Jestの導入、Github Actionsの実行など公開に必須ではないものの、実際に公開するにあたっては欲しい機能などと思います。
npmパッケージを作る際には、ぜひ参考にしてみてください。
もし気に入ったらスターなどで応援してくれると喜びます。それでは。