TypeScript で作ったパッケージを TravisCI で npm に公開するときの Tips

個人で作った [msgpack-rpc-lite][1] という Node.js 向けパッケージを npm で公開しています。その CI 環境には [GitHub][2] と [Travis CI][3] と [Codacy][4] を使っています。

当初 JavaScript で書いていたものを TypeScript で書き直しました。その時悩んだことやハマったことで学んだコツを備忘録としてまとめました。

なお、TypeScript での書き直しで行った変更点は [Comparing v0.0.6...v0.1.5 · naokikimura/msgpack-rpc-lite][0] でご覧いただけます。
## [Travis CI] `skip_cleanup: true` でデプロイ前のクリーンアップをスキップする

Travis CI のデプロイは、デフォルトではリリース前に `install` や `script` などのそれ以前のステップで追加や変更されたファイルをクリーンアップします。 `tsc` のコンパイル結果や `npm install` で取得した依存パッケージは消されてしまいます。それを回避するために `deply` の `skip_cleanup` オプションに `true` を設定します。

```diff:.travis.yml
  language: node_js
  deploy:
    provider: npm
+   skip_cleanup: true
    email: "YOUR_EMAIL_ADDRESS"
    api_key: "YOUR_AUTH_TOKEN"
    on:
      tags: true
      repo: "YOUR_REPO"
```

### ハマったこと

このような `.travis.yml` だったとき、デプロイが `sh: tsc: command not found` となって失敗しました。 `install` ステップでは失敗しなかったのに、なぜって感じでした。

```yml:.travis.yml
language: node_js
node_js:
  - "8"
deploy:
  provider: npm
  email: "YOUR_EMAIL_ADDRESS"
  api_key: "YOUR_AUTH_TOKEN"
  on:
    tags: true
    repo: "YOUR_REPO"
```

[Releasing build artifacts - npm Releasing - Travis CI][5] にちゃんと書いてありました。ドキュメントはよく読まないとダメですね。

### 参考

- [Releasing build artifacts - npm Releasing - Travis CI][5]
- [Deploying your Code - Customizing the Build - Travis CI][11]

## [npm] `.npmignore` でパッケージに含めるファイルを制御する

`.gitignore` がある場合は `.npmignore` で別途パッケージに含めないファイルを指定しましょう。 `.npmignore` がないと `.gitignore` で無視しているファイルはパッケージに含まれません(逆に言うと、必要なファイルがパッケージに含まれないことがあります)。

### ハマったこと

`tsconfig.json` で `outDir` コンパイラーオプションを設定してコンパイル結果の出力先を指定しました。 `package.json` の `main` フィールドもそれに合わせて変更しました。 また Git のトラッキング対象外するため `.gitignore` にも追記しました。 

```diff:tsconfig.json
  {
    "compilerOptions": {
      "target": "es6",
      "module": "commonjs",
+     "outDir": "./dist",
```

```diff:package.json
  {
    "name": "msgpack-rpc-lite",
    "version": "0.1.1",
-   "main": "./index.js",
+   "main": "./dist/index.js",
```

```diff:.gitignore
  coverage
  .nyc_output
  node_modules/
+ dist/
```

この状態で `npm publish` すると、`npm` はパッケージにコンパイル結果を含めてくれません。公開したパッケージを利用しようとしても `Error: Cannot find module 'msgpack-rpc-lite'` となってしまいます。

そこで `.npmignore` を `.gitignore` と同じ場所に作って、パッケージに含めたくないファイルを別途指定しました。なお、 `node_modules` は `.npmignore` には不要です。

別途指定といいましたが、実際は `.gitignore` をコピー & リネームして、パッケージに含めたいものを除外エントリーから取り除いただけです。

```:.npmignore
coverage
.nyc_output
```

### 参考

- [Note on `.gitignore` - npm Releasing - Travis CI][6]
- [Keeping files out of your package | developers | npm Documentation][8]

## [npm] `prepublish` / `prepublishOnly` / `prepare` の違いを理解する

npm にパッケージを公開するためには、その準備として TypeScript コンパイラー `tsc` を実行してコンパイル結果を出力しておく必要があります。

`npm install` の後と `npm publish` の前の両方で発生する `prepare` イベントは、 `tsc` を実行するタイミングとして適しています。

しかし、 Node.js v6 以前 (厳密に言うと npm v4.2.0 より前) は、 `prepare` イベントは発生しません。そのため、必要であれば適宜自前で `npm run prepare` と実行する必要があります。 Travis CI であれば `before_script` と `before_deploy` のステップが `prepare` イベント発生に近いタイミングです。

```yml:.travis.yml
language: node_js
node_js:
- lts/*
- 6
before_script:
- if [ $(echo "`npm -v 2>&1 | cut -d. -f-2` < 4.2" | bc) -eq 1 ]; then npm run prepare; fi
...
before_deploy:
- if [ $(echo "`npm -v 2>&1 | cut -d. -f-2` < 4.2" | bc) -eq 1 ]; then npm run prepare; fi
...
```

### 悩んだこと

TypeScript コンパイラー `tsc` を npm スクリプトで定義したいけど、どのスクリプトが適切なのかで悩みました。

Travis CI のデプロイライフサイクルは、ざっくりいうと `npm i && npm t && npm publish` だといえると思います。このライフサイクルで発火するイベントのスクリプトに `tsc` を実行するように定義したと考えました。

- `build`
- `prepublish` / `prepublishOnly` / `prepare`

悩んだ末、今回は `prepare` に定義しました。

```json:package.json
{
  "name": "msgpack-rpc-lite",
  "version": "0.1.3",
  "main": "./dist/index.js",
  "scripts": {
    ...
    "prepare": "tsc"
  },
  ...
}
```

#### `build`

よく見かけるのが、 `build` スクリプトで定義しているケース。

ただ、イベントで発火するスクリプトではない様子。 `npm install` や `npm publish` と連動して実行してほしいのに。 `npm build` としても定義したスクリプトは実行されず、 `npm run build` としないと実行されない。

...なんかしっくりこない。流儀に則っていない感がする。 Visual Studio Code を使っていて、手動ビルドやウォッチはビルドタスクで実行できるので、 `build` スクリプトとして定義しておく必要性がとくにない。ただ `tsc` を実行したいだけだし。

あと `npm build` は何に使うのかがよくわからない。

#### `prepublish` / `prepublishOnly` / `prepare`

`npm publish` に連動して実行される `prepublish` / `prepublishOnly` / `prepare` ですが、 npm のバージョンによって振る舞いがかなり異なります。詳しくは [npm prepublish の現状と今後どう変わっていくか - Qiita][10] を参照してください。

### 参考

- [PREPUBLISH AND PREPARE | scripts | npm Documentation][9]
- [npm prepublish の現状と今後どう変わっていくか - Qiita][10]
- [The Build Lifecycle - Customizing the Build - Travis CI][7]

## [TypeScript] モジュールなら宣言ファイル (.d.ts) も公開する

パッケージをモジュールとして公開するなら宣言ファイル (.d.ts) も合わせて公開しましょう。
`tsc` の `declaration` コンパイルオプションに `true` を設定し、 `package.json` の `types` フィールドに出力される宣言ファイルを設定します。

### ハマったこと

公開したパッケージを TypeScript でモジュールとして利用しようと `import` してコンパイルします。

```typescript
import * as from 'msgpack-rpc-lite';
```

すると、次のようなエラーがでます。コンパイラーが「モジュール 'msgpack-rpc-lite' の宣言ファイルが見つかりませんでした。」と怒っています。

    error TS7016: Could not find a declaration file for module 'msgpack-rpc-lite'. '~/Documents/workspace/jubaclient/node_modules/msgpack-rpc-lite/dist/index.js' implicitly has an 'any' type.
      Try `npm install @types/msgpack-rpc-lite` if it exists or add a new declaration (.d.ts) file containing `declare module 'msgpack-rpc-lite';`

そこで、モジュールの `tsconfig.json` に `"declaration": true` を追加しました。

```diff:tsconfig.json
  {
    "compilerOptions": {
      "target": "es6",
      "module": "commonjs",
+     "declaration": true,
      "sourceMap": true,
      "outDir": "./dist",
      "strict": true,
      "esModuleInterop": true
    }
    "include": [
      "src/**/*"
    ]
  }
```

加えて、 `package.json` に `"types": "./dist/index.d.ts"` を追加しました。

```diff:package.json
  {
    "name": "msgpack-rpc-lite",
    "version": "0.1.4",
    "main": "./dist/index.js",
+   "types": "./dist/index.d.ts",
```


### 参考

- [Publishing · TypeScript][12]
- [宣言ファイル(d.ts)の公開 | TypeScript 日本語ハンドブック | js STUDIO][13]

[0]: https://github.com/naokikimura/msgpack-rpc-lite/compare/v0.0.6...v0.1.5
[1]: https://www.npmjs.com/package/msgpack-rpc-lite
[2]: https://github.com/naokikimura/msgpack-rpc-lite
[3]: https://travis-ci.org/naokikimura/msgpack-rpc-lite
[4]: https://app.codacy.com/app/naokikimura/msgpack-rpc-lite/
[5]: https://docs.travis-ci.com/user/deployment/npm/#Releasing-build-artifacts
[6]: https://docs.travis-ci.com/user/deployment/npm/#Note-on-.gitignore
[7]: https://docs.travis-ci.com/user/customizing-the-build#The-Build-Lifecycle
[11]: https://docs.travis-ci.com/user/customizing-the-build/#Deploying-your-Code
[8]: https://docs.npmjs.com/misc/developers#keeping-files-out-of-your-package
[9]: https://docs.npmjs.com/misc/scripts#prepublish-and-prepare
[10]: https://qiita.com/ndxbn/items/f0cd2b13a3268254f2aa
[12]: https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html
[13]: http://js.studio-kingdom.com/typescript/declaration_files/publishing