30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

TypeScript で npm パッケージを作る

TypeScript で書いたプログラムを npm パッケージとして配布する手順を書いていく。
まだ npm パッケージの配布をしたことがない人を、想定読者としている。

よりよい書き方、詳細な設定、は措いておき、まずは最低限の要件を満たすものを作り上げる。
今回の「最低限の要件」は以下。

  • npm installyarn addでインストールできる
  • importでもrequireでもインポートすることが出来る
  • 型定義ファイルを同梱し、TypeScript アプリにもスムーズに導入できる

requireCommonJS)にも対応させるかどうかはライブラリの性質によって異なると思うが、今回は対応する。

npm パッケージに限らず、粗削りでいいから最初から最後まで動くものをまずは作り、あとから必要に応じて勉強や調査をすればいいと思っている(セキュリティやコンプライアンスに関わることは除く)。今回もその方針でいく。

この記事で利用しているライブラリのバージョンは以下。

  • npm@6.2.0
  • typescript@3.5.2
  • @types/node@12.0.10
  • dayjs@1.8.14

TypeScript の設定とプログラムの作成

何はともあれプログラムを作らないと、配布も何もない。
今回は、YYYY-MM-DD形式の文字列を渡すと、その日付の曜日を英語で返すプログラムを作る。
といっても、主な処理はライブラリに任せてしまい、自分ではほとんどコードを書かないが。

まず、パッケージの名前を決めておく。既に存在するパッケージの名前は利用できないので、使いたい名前が既に使われていないかどうか公式サイトで検索して確かめておく。
今回は実際には公開しないので何でもよいが、day-of-weekにする。

次に TypeScript をインストール。

$ yarn add typescript

次に TypeScript の設定ファイルであるtsconfig.jsonを作成。
あとで追加する項目もあるが、取り敢えずは以下の内容で進める。

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["es2018"],
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "outDir": "./dist/",
    "sourceMap": true
  },
  "include": [
    "src"
  ]
}

TypeScript については自分も初心者なので、詳しい説明は避ける。
src/の中身をコンパイルしてdist/に出力すること、出力後のコードはes5で動くものであること。
それさえ把握しておけば、この記事を読み進めるのに問題はないはず。

dist/はコンパイルしたコードを置くだけなので、.gitignoreに追加して Git の管理から外しておく。

今回は Node.js 環境でも使えるライブラリにするので、型定義ファイル@types/nodeをインストールしておく。

$ yarn add @types/node

他に、依存ライブラリとしてdayjsをインストール。

$ yarn add -D dayjs

実は今回のケースでは-Dオプションは付けてはいけないのだが、説明の都合上、敢えてこうしている。後で修正するので、そのときに説明する。

下準備が出来たのでsrc/以下にコードを書いていく。
以下の内容でsrc/index.tsを作る。

import dayjs from 'dayjs';

const DayOfWeek = (date: string): string => dayjs(date).format('dddd');

export default DayOfWeek;

これで完成したので、以降は、パッケージとしての形を整えるための作業になる。

まずはコンパイル。
$ yarn run tscでコンパイルできるので実行すると、dist/index.jsdist/index.js.mapが生成されている。
この内容でも動作はするのだが、せっかく TypeScript で作ったのだから、型定義ファイルも同梱させておきたい。そうすることで、TypeScript アプリの開発者がこのパッケージをインポートしたときに、型情報も自動的にインポートされるようになる。

tsconfig.jsondeclarationを追加すると、コンパイル時に型定義ファイルも作られるようになる。

--- a/tsconfig.json
+++ b/tsconfig.json
@@ -6,6 +6,7 @@
     "strict": true,
     "esModuleInterop": true,
     "outDir": "./dist/",
+    "declaration": true,
     "sourceMap": true
   },
   "include": [

再度コンパイルすると、以下の内容のdist/index.d.tsも生成された。

declare const DayOfWeek: (date: string) => string;
export default DayOfWeek;

これで、配布したいプログラムをdist/に生成できるようになった。
あとは、それを npm パッケージとして配布するための作業をすればよい。

package.json

パッケージの配布においては、package.jsonの記述内容が重要になる。
ライブラリをインストールした時点でpackage.json作成され、以下の内容になっているはず。

{
  "dependencies": {
    "@types/node": "^12.0.10",
    "typescript": "^3.5.2"
  },
  "devDependencies": {
    "dayjs": "^1.8.14"
  }
}

ここに、必要な項目を追加していく。

name

パッケージの名前。前述の通り、既存のパッケージと被ってはいけない。

version

パッケージのバージョン。nameversionの組み合わせで、パッケージが一意に特定される。

license

パッケージのライセンスの種類を書く。

main

ここで指定したファイルが、パッケージをインポートしたときに読み込まれることになる。

types

mainで指定したファイルに対応する型定義ファイルを、このフィールドに指定する。

files

パッケージとして配布したいファイルやディレクトリを、ホワイトリスト形式で記述していく。
今回の例だと、dist/を指定する。そうすることで、パッケージで配布する必要のないsrc/などを除外することが出来る。
package.jsonなど一部のファイルは、filesで指定した内容に影響を受けない。
https://docs.npmjs.com/files/package.json#files

上記以外にも様々なフィールドがあり、公式ドキュメントで確認できる。
docs.npmjs.com

完成形は以下。

{
  "name": "day-of-week",
  "version": "1.0.0",
  "license": "MIT",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "files": [
    "dist"
  ],
  "dependencies": {
    "@types/node": "^12.0.10",
    "typescript": "^3.5.2"
  },
  "devDependencies": {
    "dayjs": "^1.8.14"
  }
}

これでパッケージとして配布できるようになったので、次は動作確認を行う。

動作確認

動作確認のために実際にパッケージを配信するわけにはいかないので、$ npm packを使う。
このコマンドを使うことで、実際に配信することなくローカル環境で、パッケージとして問題なく機能するかどうか確認することが出来る。
早速実行してみる。

$ npm pack
npm notice 
npm notice 📦  day-of-week@1.0.0
npm notice === Tarball Contents === 
npm notice 282B package.json     
npm notice 77B  dist/index.d.ts  
npm notice 409B dist/index.js    
npm notice 243B dist/index.js.map
npm notice === Tarball Details === 
npm notice name:          day-of-week                             
npm notice version:       1.0.0                                   
npm notice filename:      day-of-week-1.0.0.tgz                   
npm notice package size:  735 B                                   
npm notice unpacked size: 1.0 kB                                  
npm notice shasum:        ec082bdcf157f89045c19737ec853ea3ddc47dc2
npm notice integrity:     sha512-SY1YfL2l+eG67[...]K1JoGgnMn/sHQ==
npm notice total files:   4                                       
npm notice 
day-of-week-1.0.0.tgz

day-of-week-1.0.0.tgzというファイルが作成され、package.jsonの他、filesで指定したdist/が含まれていることが分かる。

この.tgzファイルをパッケージとして指定してインストールすることで、動作確認が出来る。
何か適当に新しいプロジェクトを作り、試してみる。

$ yarn add day-of-week-1.0.0.tgzのパスを指定

そして、動作確認用のプログラムをindex.jsとして書く。

const DayOfWeek = require('day-of-week').default;

console.log(DayOfWeek('2019-06-28'));

満を持して$ node index.jsを実行、すると、エラーになる。

$ node index.js 
internal/modules/cjs/loader.js:583
    throw err;
    ^

Error: Cannot find module 'dayjs'

dayjsがないと怒られるのでyarn.lockを確認してみると、day-of-weekの他にtypescript@types/nodeは入っているが、dayjsが入っていない。

依存関係について

これは、dayjsをインストールする際に$ yarn add -D dayjsとしてしまった(この説明をするためにわざとそうしたのだが)のが原因。

パッケージをインストールしたときに一緒にインストールされるのはdependenciesに書かれているライブラリのみで、devDependenciesはインストールされない。

なので、dayjsdependenciesにする。

$ yarn remove dayjs
$ yarn add dayjs

これで修正完了。
package.jsonversion1.0.1にした上で、$ npm packを行う。
day-of-week-1.0.1.tgzが生成されるので、それを、動作確認用のプロジェクトでインストールする。

改めて$ node index.jsを実行すると、今度は正しく動いた。

$ node index.js 
Friday

このように、開発するパッケージの依存関係には注意する必要がある。
依存関係ついては TypeScript の公式ドキュメント にも記述がある。

簡単に動作確認できるのでrequireを使ったが、importすることも出来るし、TypeScript なら型チェックも行われる。

import DayOfWeek from 'day-of-week';

// Friday
console.log(DayOfWeek('2019-06-28'));

// error TS2322: Type 'string' is not assignable to type 'number'.
const result: number = DayOfWeek('2019-06-28');

公開作業

無事にパッケージを作れたので、ここから先は、公開(パブリッシュ)のための作業を行っていく。
この手順通りに作業すると実際に公開されてしまうので、注意すること。

npm scriptsprepublishOnlyを設定する。
このスクリプトは、パブリッシュの前に必ず実行される。なので、ビルド作業などを設定しておくとよい。
package.jsonに以下の内容を追記。

  "scripts": {
    "prepublishOnly": ビルド作業など
  },

ただ、prepublishOnlyには類似のコマンドが複数あるうえ、npm cliのバージョンによって挙動が異なるらしい。 実際に使う際にはよく確認しておく。
参考:npm の prepublish と prepare の変遷 - Qiita

パブリッシュのためには npm のアカウントが必要なので、まだ持っていない場合は作成する。

Sign Up - npm

$ npm loginでログインする。

ログインしているかどうかは、$ npm whoamiで確認できる。ユーザー名が表示されたら、そのユーザーでログインしている。

$ npm whoami
ユーザー名

あとは、パッケージのルートディレクトリで$ npm publishを実行すれば、prepublishOnlyのあとに、パブリッシュが実行される。
$ npm install$ yarn addでそのパッケージをインストールできれば成功。

パブリッシュが終わったあとは$ npm logoutで忘れずにログアウトしておく。

まとめ

npm パッケージを公開すること自体はすごく簡単に出来る。

t-wada さんも、細かすぎて伝わらない package.json 小ネタ三選という記事でこう言っている。

Node.js のエコシステムの豊穣さは、モジュール利用者からモジュール作成者に進むためのハードルが低いことで成り立っています。このエントリを読んだ皆さんも、ぜひ npm author になってみてください。そのハードルを越えるのは、意外と難しくありません。

ということで、自分以外の誰かにも役立ちそうなプログラムを書けたときは、積極的にパッケージとして公開していこう。

参考資料