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 になってみてください。そのハードルを越えるのは、意外と難しくありません。

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

参考資料

Error Boundary で React アプリ内のエラーを捕捉する

Error Boundary は React のv16から導入された機能で、これを使うとコンポーネント内で発生したエラーをキャッチすることが出来る。

主に、エラー用のUIを表示したり、エラーを記録したりすることに使われる。
前者にはstatic getDerivedStateFromErrorというメソッドを、後者にはcomponentDidCatchというメソッドを用いる。
Error Boundary のためのクラスコンポーネントを作り、そこにこれらのメソッドを定義して使う。

この記事では、バージョン16.8.6で動作確認している。

動作確認用のアプリの用意

以下の内容でアプリを作る。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Template htmlfile</title>
</head>
<body>
  <p>↓この下に React アプリが表示される↓</p>
  <div id="app"></div>
  <p>↑この上に React アプリが表示される↑</p>
</body>
</html>
import React from 'react';
import ReactDOM from 'react-dom';

const AquaChild = () => (
  <div style={{backgroundColor: 'white'}}>aqua child</div>
);

const Aqua = () => (
  <div style={{backgroundColor: 'aqua', padding: '10px'}}>
    aqua
    <AquaChild />
  </div>
);
const Lime = () => <div style={{backgroundColor: 'lime'}}>lime</div>;

const App = () => (
  <>
    <Aqua />
    <Lime />
  </>
);

ReactDOM.render(<App />, document.querySelector('#app'));

これをビルドすると以下のようなUIを持ったアプリが出来るので、これを対象に検証していく。

f:id:numb_86:20190615221816p:plain

エラー用のUIを表示させる

React はv16から、発生したエラーがキャッチされなかった場合、コンポーネントツリー全体をアンマウントするようになった。
公式ドキュメントによればこれは、壊れたUIを表示することは何も表示しないことよりも悪いことである、という考えによるもの。

試しに、先程作ったサンプルでエラーを発生させてみる。

 const Aqua = () => (
   <div style={{backgroundColor: 'aqua', padding: '10px'}}>
-    aqua
+    aqua{x}
     <AquaChild />
   </div>
 );

Aquaのなかでxを参照しているが、xは存在しない変数なのでReferenceErrorが発生する。
この結果、アプリ全体が表示されなくなる。

f:id:numb_86:20190615221856p:plain

エラーが発生したAquaだけでなく、Limeも含めたコンポーネントツリー全体がアンマウントされているのが分かる。

static getDerivedStateFromErrorを使ってエラーをキャッチすることで、ツリー全体がアンマウントされるのを防ぎ、適切なUIを表示させることが出来る。

Error Boundary を使うため、以下のコンポーネントを作成した。

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = {hasError: false};
  }

  static getDerivedStateFromError(error) {
    console.log(error instanceof Error);
    return {hasError: true};
  }

  render() {
    const {state, props} = this;
    if (state.hasError) {
      return (
        <div style={{backgroundColor: 'orange'}}>
          背景がオレンジのコンポーネントは ErrorBoundary
          によって表示されているエラー用のコンポーネントです。
        </div>
      );
    }
    return props.children;
  }
}

static getDerivedStateFromErrorの引数で、キャッチしたエラーオブジェクトを取得できる。

そしてこのコンポーネントで、AquaLimeをそれぞれラップする。

 const App = () => (
   <>
-    <Aqua />
-    <Lime />
+    <ErrorBoundary>
+      <Aqua />
+    </ErrorBoundary>
+    <ErrorBoundary>
+      <Lime />
+    </ErrorBoundary>
   </>
 );

こうすることで、AquaLimeのなかでエラーが発生した際に、それをキャッチできる。

この状態でページを表示すると、今度は以下のようになっている。

f:id:numb_86:20190615221920p:plain

Aquaで発生したエラーをキャッチしてエラー用のUIが表示されている。
その一方で、Limeではエラーが発生していないので、そのまま表示されている。

このように、Error Boundary を上手く使うことでアプリ全体がクラッシュするのを防ぎ、適切なUIをユーザーに提供できるようになる。

Error Boundary の直下ではなくもっと深い階層でエラーが発生しても、問題なくキャッチできる。

 const AquaChild = () => (
-  <div style={{backgroundColor: 'white'}}>aqua child</div>
+  <div style={{backgroundColor: 'white'}}>aqua child{x}</div>
 );
 
 const Aqua = () => (
   <div style={{backgroundColor: 'aqua', padding: '10px'}}>
-    aqua{x}
+    aqua
     <AquaChild />
   </div>
 );

AquaではなくAquaChildでエラーを発生させても、先程と同じ表示になる。

f:id:numb_86:20190615221920p:plain

コンポーネントのなかでエラーが発生すると、ツリーを上に辿っていき、一番最初に到達した ErrorBoundary がエラーをキャッチする仕組みになっている。つまり、JavaScript のcatch{}と同じような挙動である。
最後までキャッチされなかった場合は、既に述べたようにツリー全体がアンマウントされる。

Error Boundary の対象になるのは、配下のコンポーネントで発生したエラーのみ。
Error Boundary 自身のなかで発生したエラーは、キャッチすることが出来ない。
例えば以下のようにすると、キャッチできずにツリー全体がアンマウントされてしまう。

 const AquaChild = () => (
-  <div style={{backgroundColor: 'white'}}>aqua child{x}</div>
+  <div style={{backgroundColor: 'white'}}>aqua child</div>
 );

const App = () => (
   <>
     <ErrorBoundary>
       <Aqua />
+      <div>{x}</div>
     </ErrorBoundary>
     <ErrorBoundar

このエラーをキャッチしたければ、Appをラップする必要がある。

-ReactDOM.render(<App />, document.querySelector('#app'));
+ReactDOM.render(
+  <ErrorBoundary>
+    <App />
+  </ErrorBoundary>,
+  document.querySelector('#app')
+);

そうするとAppの代わりにエラー用のUIが表示されるようになる。

f:id:numb_86:20190615221954p:plain

エラーを記録する

エラーをログに残したり、エラー監視サービスなどに送信したりする場合は、Error Boundary にcomponentDidCatchメソッドを定義して、そのなかで行う。

     return {hasError: true};
   }
 
+  componentDidCatch(error, info) {
+    console.log(error);
+    console.log(info.componentStack);
+  }
+
   render() {
     const {state, props} =

第一引数で、キャッチしたエラーオブジェクトを取得できる。
第二引数のinfocomponentStackを持っており、ここには、エラーが発生したコンポーネントのスタックトレースが入っている。

以下は、AquaChildでエラーが発生した際のスタックトレース。

    in AquaChild (at src/index.js:13)
    in div (at src/index.js:11)
    in Aqua (at src/index.js:21)
    in ErrorBoundary (at src/index.js:20)
    in App (at src/index.js:29)

static getDerivedStateFromErrorとの違いだが、公式のAPIリファレンスによれば、static getDerivedStateFromErrorはUIの描画のために使い、副作用を扱う場合にcomponentDidCatchを使うとよいらしい。

キャッチできないエラー

既に述べたように Error Boundary は自身のエラーをキャッチすることが出来ないが、配下のコンポーネントのエラーでもキャッチしないものが2つある。

1つ目は、イベントハンドラ内でのエラー。
以下のようにボタンを押した際にエラーが発生するようにした場合、レンダリング時にはエラーを出さないので、そのまま表示される。

   <div style={{backgroundColor: 'aqua', padding: '10px'}}>
     aqua
     <AquaChild />
+    <button
+      type="button"
+      onClick={() => {
+        x;
+      }}
+    >
+      error button
+    </button>
   </div>

f:id:numb_86:20190615222013p:plain

そしてボタンを押すとエラーが発生するのだが、このエラーはキャッチされず、表示にも影響がない。

2つ目が、非同期処理のなかでのエラー。これも、エラーはどこにもキャッチされないまま終わり、アプリはそのまま表示され続ける。

const Aqua = () => {
  Promise.resolve().then(() => x);
  return (
    <div style={{backgroundColor: 'aqua', padding: '10px'}}>
      aqua
      <AquaChild />
    </div>
  );
};

参考資料