npm を TypeScript で実装する
自作 npm xlsx-extractor を JavaScript から TypeScript へ移行した際の覚書。はじめて npm 開発した時の npm icon-gen v1.0.0 release を踏まえて、新規 npm を TypeScript で実装する場合の入門記事として機能することを考慮しながら書く。
パッケージ仕様と開発方針
開発対象となる xlsx-extractor の仕様と開発方針をまとめる。
仕様
- Excel ファイル (
*.xlsx
形式) を解析してシート情報とセルを取得する - 空のセルは詰めずに最大の列と行で埋める
- Excel 上のセル座標を維持する
- 例えば B7 に値がある場合、0 開始の座標で B =
1
7 =6
として常に[1][6]
で取得可能とする
- インターフェースは Node.js 用と CLI の 2 種類へ対応
- CLI は標準出力にシート内容を JSON 形式で出力する
開発方針
- TypeScript で実装、Node.js LTS (現時点の最新は 10) 向け JavaScript と型ファイルをビルドして提供
- 型ファイルをパッケージに同梱することで
@types/XXXX
なしに npm 単体で型を参照可能となる - 本パッケージを npm として参照する際に vscode 上の開発が楽になる
- 型ファイルをパッケージに同梱することで
- ユニット テストを書く
- テスト ランナーは Jest を採用
- テストは元コードと併置する、例えば
index.ts
のテストは同一階層のindex.test.ts
- 継続的にテストを自動実行する
- サービスは Travis CI を採用
- OSS なら無料
- コード書式を明示的に管理する
README.md
だけでパッケージの機能を把握できるようにするCHANGELOG.md
に更新履歴を記録するLICENSE
ファイルにライセンスを明記する、本プロジェクトの場合は MITexamples/
に実動サンプルを公開する- Node.js と TypeScript 向けを用意
- TypeScript 版を vscode で開けば型情報の便利さを実感してもらえるはず
プロジェクトのファイル構成
プロジェクトのファイル構成をまとめる。
.
├── dist/
├── examples/
├── src/
│ ├── bin/
│ └── lib/
├── .gitignore
├── .prettierrc
├── .travis.yml
├── jest.config.js
├── LICENSE
├── CHANGELOG.md
├── README.md
├── package.json
└── tsconfig.json
それぞれの役割は以下。
名前 | 内容 |
---|---|
dist/ |
TypeScript からビルドされたリリース用 JavaScript や型ファイルを格納するディレクトリー。 |
examples/ |
パッケージの実動サンプルを格納するディレクトリー。 |
src/ |
TypeScript による実装コードを格納するディレクトリー。 |
src/bin/ |
CLI インターフェース実装ディレクトリー。 |
src/lib/ |
パッケージ本体の実装ディレクトリー。 |
.gitignore |
Git 管理から除外するファイルとディレクトリーの設定ファイル。 |
.prettierrc |
コード フォーマッター Prettier の設定ファイル。 |
.travis.yaml |
継続的インテグレーション サービス Travis CI の設定ファイル。 |
jest.config.js |
テスト フレームワーク Jest の設定ファイル。 |
LICENSE |
パッケージのライセンス ファイル。 |
CHANGELOG.md |
パッケージの更新履歴ファイル。 |
README.md |
パッケージ情報ファイル。 |
package.json |
プロジェクト設定ファイル。 |
tsconfig.json |
TypeScript 設定ファイル。 |
ソース コードの格納先として開発用を src/
、ビルドされたものは dist
とする理由について。
ビルドせず開発用のソース コードを直に公開せず (できず) TypeScript や Banel によるビルドを経由するならルート階層を分けることで、ビルド対象と出力 (公開) するもののフィルター処理がおこないやすくなる。
実際の効能は後述する package.json
や tsconfig.json
の解説を読めば理解できるだろう。例えばビルド出力先を dist/
というディレクトリー配下にすることで、package.json
の files
設定をこれのみで済ませている。
プロジェクト設定
プロジェクト設定となる package.json
定義。
{
"name": "xlsx-extractor",
"description": "Extract the colums/rows from XLSX file.",
"version": "1.4.1",
"author": "akabeko (http://akabeko.me/)",
"license": "MIT",
"homepage": "https://github.com/akabekobeko/npm-xlsx-extractor#readme",
"engines": {
"node": ">= 10"
},
"main": "dist/lib/index.js",
"bin": "dist/bin/index.js",
"files": [
"dist"
],
"keywords": [
"XLSX",
"Excel",
"Extract"
],
"repository": {
"type": "git",
"url": "git+https://github.com/akabekobeko/npm-xlsx-extractor.git"
},
"bugs": {
"url": "https://github.com/akabekobeko/npm-xlsx-extractor/issues"
},
"scripts": {
"test": "jest",
"start": "npm run watch",
"tsc": "tsc --noEmit",
"build": "tsc",
"watch": "tsc -w",
"prepare": "npm run build"
},
"dependencies": {
"commander": "^2.20.0",
"node-zip": "^1.1.1",
"xml2js": "^0.4.19"
},
"devDependencies": {
"@types/jest": "^24.0.15",
"@types/node": "^12.0.10",
"@types/xml2js": "^0.4.4",
"jest": "^24.8.0",
"ts-jest": "^24.0.2",
"typescript": "^3.5.2"
}
}
それぞれの役割。
設定 | 解説 |
---|---|
name |
パッケージ名。 |
description |
パッケージが提供する機能の説明。 |
version |
バージョン情報。 |
author |
作者。 |
license |
ライセンス情報。 |
homepage |
プロジェクトのホームページ URL。 |
engines |
動作環境。今回は Node.js 向けなので、それ以降を指定。 |
main |
パッケージがモジュール参照 (import や require ) された際のエントリー ポイントになるファイル。ビルドで出力されたものを指定。 |
bin |
パッケージが CLI として参照された際のエントリー ポイントになるファイル。ビルドで出力されたものを指定。 |
files |
パッケージを公開する際に package.json などの必須ファイル以外で対象に含めるもの。ビルド成果物の出力先ディレクトリーのみを指定。 |
keywords |
パッケージの機能や内容をあらわすキーワード。パッケージ マネージャーによる検索などで参照される。 |
repository |
ソース コードの格納場所。本リポジトリーは GitHub で運用しているため、その情報を指定。 |
bugs |
パッケージのバグ情報などが公開されている場所。GitHub 運用しているため、その Issues を指定。 |
scripts |
npm-scripts。開発用の処理をシェル コマンドとして定義。ここに定義されたものは npm run XXXX で呼び出せる。start や test などの特別な予約済みのものは run を省略して npm start とすることも可能。 |
dependencies |
パッケージが依存する npm のうち、ユーザー環境でも必要なもの。npm i でインストールするとこちらへ追加される。 |
devDependencies |
パッケージが依存する npm のうち、開発環境のみが必要とするもの。npm i -D でインストールするとこちらへ追加される。例えば TypeScritp ビルドやユニット テストは開発に使用するものでユーザーには不要。 |
TypeScript ビルド設定
TypeScript をビルドする方法は大まかに二通り。
typescript
のtsc
コマンド- Babel +
@babel/preset-typescript
Web フロントエンド開発だと複数 Web ブラウザーの ES (ECMAScript) 実装差を埋めるために @babel/preset-env
を利用したいから 2 を選んでいる。しかし今回の npm は Node.js 専用なので単一環境だけ考慮すればよい。そのため 1 を採用した。
TypeScript に関する設定は tscofig.json
へ定義する。
{
"compilerOptions": {
"target": "es2017",
"module": "commonjs",
"moduleResolution": "node",
"outDir": "dist",
"strict": true,
"allowSyntheticDefaultImports": true,
"declaration": true,
"esModuleInterop": true,
"sourceMap": false
},
"include": ["src/**/*"],
"exclude": ["src/**/*.test.ts"]
}
compilerOptions
の内訳。
設定 | 解説 |
---|---|
target |
コンパイルされた JavaScript の対象環境。今回は Node.js 10 を想定している。Node.js ES2015/ES6, ES2016 and ES2017 support を見ると ES2017 が最新の安全圏と判断して採用。 |
module |
コンパイル後のモジュール参照方法。Node.js 向けにコンパイルするので commonjs を選択。 |
moduleResolution |
開発用ソース コードにおけるモジュール参照の解決方法。Node.js 向け開発なので node を選択。 |
outDir |
コンパイルされた成果物の出力先ディレクトリー。dist/ を指定。 |
strict |
noImplicitAny など厳密な型チェック系の設定をすべて有効にするフラグ。こうしたルールは「基本を厳しく例外的にゆるめる」としておくのが安全なので有効化。 |
allowSyntheticDefaultImports |
defalt export のないモジュールからの暗黙的な import を許可するフラグ。型チェックにしか使用されないそうだが、有効化。 |
declaration |
コンパイル時に型情報ファイル *.d.ts を生成するフラグ。npm 単体で型情報を提供するため添付したいので有効化。 |
esModuleInterop |
モジュール参照における CommonJS と ES Modules の相互運用フラグ。Node.js 向けなので有効化。 |
sourceMap |
Source Maps を生成するフラグ。不要なので無効化。vscode の Node.js デバッガーや Web ブラウザー向け開発で DevTools デバッガーを利用したいなら生成する。 |
その他の設定。
設定 | 解説 |
---|---|
include |
コンパイル対象の指定。開発用ソース コードは src/ に配置するので、それ以下のすべてを対象。 |
exclude |
コンパイル対象から除外するものの指定。ユニット テスト用ファイルすべてを指定。 |
npm-scripts
ビルド処理などを定義した npm-scripts について。
{
"scripts": {
"test": "jest",
"start": "npm run watch",
"tsc": "tsc --noEmit",
"build": "tsc",
"watch": "tsc -w",
"prepare": "npm run build"
}
}
TypeScript に関する処理は npm typescript
が提供する CLI tsc
を利用する。tsc
は tsconfig.json
を参照するため、共通設定はそちらへ集約しておく。目的に応じて分岐したい設定だけ npm-scripts で指定する方針。各コマンドの役割をまとめる。
コマンド | 役割 |
---|---|
test |
Jest によるユニット テストを実行する。 |
start |
watch コマンドを間接的に呼び出す。 |
tsc |
TypeScritp をビルドせず、コンパイル チェックだけ実行する。 |
build |
TypeScript をビルドする。 |
watch |
TypeScript ビルドをファイル監視モードで起動する。対象ファイルが保存されるたびに逐次、差分ビルドが実行される。モード終了は Control + C。 |
ユニット テスト
ユニット テストは Jest を採用。TypeScritp を対象とする場合なら npm として jest
と ts-jest
が必要。設定は jest.config.js
に定義する。
module.exports = {
verbose: true,
transform: {
'^.+\\.ts$': 'ts-jest'
}
}
設定内容は以下。
設定 | 内容 |
---|---|
verbose |
テスト実行時、内訳を詳細に表示。有効にするとテスト コード中の console も出力される。 |
transform |
テスト対象の変換設定。TypeScript ファイルを対象として ts-jest で変換するように指定。 |
テストと対象コードは同じディレクトリーに併置している。例えば index.ts
に対するテストは index.test.ts
とした。開発中はテストと対象コードを往復しがちなので、これらの距離が近いとアクセスしやすくて便利。この運用を想定してか Jest は標準で *.test.js
や *.test.ts
を暗黙的にテストとみなす。
テスト コード記法は Mocha 風の describe
- it
形式を採用。チェックも expect
ではなく assert
にした。これは、長く Mocha を利用して馴染みがあるのと記法の互換を高めておけば相互に乗り入れるのが容易になるだろう、という理由による。例えば
import assert from 'assert'
import { createEmptyCells, getSheetSize, numOfColumn } from './xlsx-util'
describe('XlsxUtil', () => {
describe('createEmptyCells', () => {
it('Create empty cells', () => {
const rows = 10
const cols = 5
const cells = createEmptyCells(rows, cols)
assert.strictEqual(cells.length, rows)
assert.strictEqual(cells[0].length, cols)
})
})
})
は TypeScript か ES Modules が有効な環境なら Jest と Mocha どちらでも動く。なお assert
にしてもテスト失敗時は Jest による詳細なエラー情報が表示されるので使用感はそのままだ。
動作検証
実際にパッケージを動作させての検証について。
npm pack
公開されたパッケージをインストールして参照した状態を再現するためコマンドとして npm pack
を利用可能。これをプロジェクトのルートで実行すると xlsx-extractor-1.4.1.tgz
のようにパッケージ名とバージョンで命名された *.tgz
ファイルが生成される。
$ npm pack
...中略
xlsx-extractor-1.4.1.tgz
このファイルを別の Node.js プロジェクトにコピーして以下のコマンドを実行。パッケージ名の代わりにファイルを指定している点に注目。
$ npm i xlsx-extractor-1.4.1.tgz
するとまるで公開されたパッケージのように追加される。
{
"dependencies": {
"xlsx-extractor": "file:xlsx-extractor-1.4.1.tgz"
}
}
参照も import xlsx-extractor
や require('xlsx-extractor')
で OK。あとはエンド ユーザーとしてパッケージ利用したことを想定したコードを書いて検証すればよい。なお検証コードが TypeScript なら、パッケージに同梱した型情報ファイルが機能しているかを vscode 上で確認可能。
npm pack
で生成した npm をインストールすると dependencies
内のバージョン値こそ特殊になるが、アンインストールは通常どおり npm un xlsx-extractor
でよい。
npm link
公開されたパッケージを CLI として呼び出した状態を再現するためのコマンドとして npm link
がある。これをプロジェクトのルートで実行すると、ローカル PC のグローバルな npm としてプロジェクトにリンクを貼ってくれる。
$ npm link
...中略
/usr/local/bin/xlsx-extractor -> /usr/local/lib/node_modules/xlsx-extractor/dist/bin/index.js
/usr/local/lib/node_modules/xlsx-extractor -> /Users/XXXX/Documents/dev/node/xlsx-extractor
この状態で CLI を呼び出してみると検証がおこなえる。ただしプロジェクト側のコードを編集してビルドしても反映されないので、なにか修正したら npm link
を再実行すること。
$ xlsx-extractor -v
1.4.1
リンクを解除したい場合はプロジェクトのルートで npm unlink
を実行。
$ npm unlink
removed 1 package in 1.682s
実際の開発は以下のような感じになるだろう。
npm start
またはnpm run watch
でビルドをファイル監視モードで起動- コードを編集して保存
- 検証したいビルドができたら
npm link
- 2 〜 3 を繰り返す
- ひととおり検証したらファイル監視モードを終了させて
npm unlink
examples/
ここまでの手順を実行しやすいように examples/
ディレクトリーを用意する。本プロジェクトの構成は以下。
.
├── README.md
├── index.js
├── index.ts
├── package.json
└── sample.xlsx
名前 | 内容 |
---|---|
README.md |
このディレクトリーを利用した検証方法の解説書。 |
index.js |
Node.js としての検証コード。 |
index.ts |
TypeScript with Node.js としての検証コード。 |
package.json |
プロジェクト設定ファイル。 |
sample.xlsx |
検証に使用するサンプル Excel ファイル。 |
package.json
の定義。
{
"name": "xlsx-extractor-examples",
"description": "Examples for the xlsx-extractor.",
"private": true,
"version": "1.0.0",
"author": "akabeko (http://akabeko.me/)",
"license": "MIT",
"main": "index.js",
"scripts": {
"js": "node index.js",
"ts": "ts-node index.ts"
},
"dependencies": {
"ts-node": "^8.3.0",
"typescript": "^3.5.2",
"xlsx-extractor": "^1.4.0"
}
}
xlsx-extractor
はパッケージ公開されたものを参照する。例えば素の Node.js 向けだとこんな感じ。
const xlsx = require('xlsx-extractor')
// Get sheets count
console.log(xlsx.getSheetCount('./sample.xlsx'))
// Single
xlsx
.extract('./sample.xlsx', 1)
.then((sheet) => {
console.log(sheet)
})
.catch((err) => {
console.log(err)
})
example/
はエンド ユーザー環境を想定している。そのためパッケージ本体のバージョンを更新したら、こちらへも反映すること。
検証コードを直に実行せず npm-scripts 経由としている点も重要。npm-scripts はプロジェクトのローカルにインストールされた npm を参照可能なので、検証に必要なものを package.json
に定義しておけば examples/
内で環境が完結する。環境構築の手順も簡単で
cd examples
npm i
を実行するだけ。以降は npm run js
と npm run ts
を実行すれば検証になる。開発時の利用方法は以下のような感じ。
- Node.js 向けは検証コードから直に
../dist/bin/index.js
を参照して検証、きりのよいところでnpm pack
したものを参照 (通常のパッケージ参照へ戻す) して再検証 - CLI 向けは
npm link
後にここのサンプル データを利用して検証
型情報のない npm 参照について
tsconfig.json
で "strict": true
や個別に "noImplicitAny": true
した場合、外部モジュールを参照した際に型情報ファイルが同梱されていないとエラーになる。その際は @types
から追加してゆくのだが (vscode からもそのように促される)、ここにも公開されていなければ以下の方法で対応可能。
tsconfig.json
で"noImplicitAny": false
にする- TypeScript再入門「がんばらないTypeScript」で、JavaScriptを“柔らかい”静的型付き言語に(gfx執筆) - エンジニアHub|若手Webエンジニアのキャリアを考える! などで紹介されている方法
- 型情報のないものを
any
型としてエラーにしない - 利用している npm が多すぎて以降の対応が面倒なら採用
- 私は「標準を厳しく例外的にゆるくする」運用が好みなので避ける
declare module 'モジュール名'
を定義する- プロジェクト内でグローバルに
any
扱いになる - これをどこに定義するか?というのを決めるのが難しい
src/@types/index.d.ts
とかに定義するのを見かけるdeclare module
を定義したファイルにexport XXXX
を併記するとエラーになる問題あり
- プロジェクト内でグローバルに
import
の代わりにrequire
で参照する- 参照したファイル内でのみ
any
扱いになる - 影響範囲が小さく定義位置に迷うこともないので
declare module
よりもこちらのほうが好み - 本プロジェクトでもこれを採用、node-zip 参照を
require
にしている
- 参照したファイル内でのみ
まとめ
今回の npm は規模の小さなプロジェクトだが、それでも TypeScript 導入の効果を実感できた。具体的には
- TypeScript で書き直すことにより型が明示され、コードの可読性が向上した
- 型が強制されることでダックタイピング任せの雑な設計を見直す機会を得られた
- vscode 上の開発体験が向上
- TypeScript 標準で ESLint 並に警告やエラーをリアルタイムに指摘してくれる
- JavaScript でも JSDoc などから型推論してくれるが TypeScript のほうが圧倒的に正確ではやい
という感じ。
以前の Babel を利用した開発環境と比べてて後退した部分としては @babel/preset-env
による明示的な Node.js バージョン指定ぐらい。
これについては Node.js なら単体の下限バージョンを想定すればよいので、その安全圏になる ES バージョンを大雑把に指定しておけば十分。Node.js としても重要度の高い機能は積極的に対応されているので、普通に書いている分には非互換の問題には遭遇しないだろう。
よい感触が得られたので、他の自作 npm も順次 TypeScript 化してゆく予定。