1. suzuki_sh

    Posted

    suzuki_sh
Changes in title
+TypeScriptでCLIツールを作って、npmパッケージにする
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,295 @@
+## 環境
+
+- node 9.4.0
+- TypeScript 2.9.2
+- npm 6.1.0
+- webpack 4.16.3
+- Visual Studio Code 1.25.1
+
+予め[tj/n: Node version management](https://github.com/tj/n)等を使って、Node.jsをインストールしておきましょう。
+
+## 実践
+適当な名前でプロジェクトディレクトリを作っておきます。ココでは`ts-sample`とします。
+
+```
+$ mkdir ts-sample
+$ cd ts-sample/
+$ git init
+$ curl https://raw.githubusercontent.com/github/gitignore/master/Node.gitignore > .gitignore
+```
+
+### TypeScript + webpackでBuild環境構築
+まず`package.json`を作成します。
+
+```
+$ npm init
+```
+適当に答えていくと、こんな感じになります。
+
+```json:package.json
+{
+ "name": "ts-sample",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "",
+ "license": "MIT"
+}
+```
+
+TypeScriptと、ビルドするために必要なパッケージを`devDependencies`にインストールします。
+今回はwebpackを使うため、`ts-loader`もインストールします。
+
+```
+$ npm install --save-dev webpack webpack-cli typescript ts-loader
+```
+TypeScriptの設定である`tsconfig.json`を作成します。
+
+```
+$ ./node_modules/typescript/bin/tsc --init
+```
+
+webpackの設定である`webpack.config.js`を作成します。
+
+```js:webpack.config.js
+module.exports = {
+ entry: './src/index.ts',
+ target: 'node',
+ module: {
+ rules: [
+ {
+ test: /\.ts$/,
+ use: ['ts-loader'],
+ exclude: /node_modules/
+ }
+ ]
+ },
+ resolve: {
+ extensions: [ '.tsx', '.ts', '.js' ]
+ }
+};
+```
+
+### ソースコードの記述
+TypeScriptのコーディングを始める前に、Node.js自体の型定義ファイルをインストールしておきます。
+
+```
+$ npm install --save-dev @types/node
+```
+実際のソースコードを書いていきます。
+
+```
+$ mkdir src
+$ code src/index.ts
+```
+今回は例として、コマンドライン引数から数字を受け取って、[Fizz Buzz](https://ja.wikipedia.org/wiki/Fizz_Buzz)で返すだけのCLIプログラムを書いてみます。
+
+```ts:src/index.ts
+const num : number = +process.argv[2];
+console.log(fizzbuzz(num));
+
+function fizzbuzz(num : number) : string {
+ if (num % 15 == 0) {
+ return "FizzBuzz";
+ } else if (num % 3 == 0) {
+ return "Fizz";
+ } else if (num % 5 == 0) {
+ return "Buzz";
+ }
+ return num.toString();
+}
+
+```
+
+### ビルドして実行
+webpackでビルドするnpm scriptを記述します。
+
+```diff:package.json
+ "scripts": {
++ "build": "webpack",
+```
+ビルドします。
+
+```
+$ npm run build
+```
+実行してみます
+
+```
+$ node dist/main.js 1
+1
+$ node dist/main.js 2
+2
+$ node dist/main.js 3
+Fizz
+```
+うまく動きました。
+
+### npm経由でコマンドとして実行
+コマンドとして実行できるように設定していきます。
+
+まずpackage.jsonに`bin`の設定を追加します。
+
+```diff:package.json
++ "bin": {
++ "ts-sample": "./bin/ts-sample.js"
++ },
+```
+参考: [package\.json \| npm Documentation](https://docs.npmjs.com/files/package.json)
+
+設定した`bin/ts-sample.js`ファイルからプログラムを実行できるようにします。
+
+```js:bin/ts-sample.js
+#!/usr/bin/env node
+require('../dist/main.js');
+```
+`$ npm link`コマンドを使うと、このプロジェクトに対してシンボリックリンクを貼り、ローカルでnpm moduleをglobal installしたかのように使うことができるようになります。
+
+```
+$ npm link
+```
+参考: [link \| npm Documentation](https://docs.npmjs.com/cli/link)
+
+こうして`package.json`内で`bin`に指定したキーで、コマンドが実行になります。
+
+```
+$ ts-sample 15
+FizzBuzz
+```
+
+### 開発/本番の各ビルドステージの設定
+現在出力された`js:dist/main.js`は、プロダクションビルドの設定になっています。
+中身はminifyされており単体では読むことができません。
+
+```js:dist/main.js
+!function(e){var r={};function t(n){if(r[n])return r[n].exports;var o=r[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=r,t.d=function(e,r,n){t.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:n})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,r){if(1&r&&(e=t(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(t.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var o in e)t.d(n,o,function(r){return e[r]}.bind(null,o));return n},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,"a",r),r},t.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},t.p="",t(t.s=0)}([function(e,r,t){"use strict";var n=+process.argv[2];console.log(function(e){if(e%15==0)return"FizzBuzz";if(e%3==0)return"Fizz";if(e%5==0)return"Buzz";return e.toString()}(n))}]);
+```
+また、開発時はビルド用になんどもコマンドを打つのも面倒です。
+
+- 開発時にはファイルの変更を監視したい
+- 開発時にはソースマップを有効にしたい
+- 本番ビルドではソースマップを無効にしたい
+- 後に`npm publish`する直前に自動で本番ビルドしたい
+
+これらの問題を解決するため、webpackの設定を開発用と本番用で分離します。
+`webpack.dev.js`を作成し、`webpack.config.js`を読み込んで設定をmergeします。
+
+まずmergeするためのライブラリを読み込みます。
+
+```
+$ npm install webpack-merge --save-dev
+```
+
+`webpack.dev.js`を作成し、下記の記述をします。
+
+```js:webpack.dev.js
+const merge = require('webpack-merge');
+const common = require('./webpack.config.js');
+module.exports = merge(common, {
+ mode: 'development',
+ devtool: 'inline-source-map',
+});
+```
+
+`package.json`に、それぞれのビルド用のコマンドを追記します
+
+```diff:package.json
+ "scripts": {
++ "prepare": "webpack --config webpack.config.js",
++ "watch": "webpack --config webpack.dev.js --watch"
+ },
+```
+こうして、開発時は`npm run watch`することでファイル変更時に自動でビルドされるようになりました。
+
+```
+$ npm run watch
+```
+このときは`dist/main.js`の中身も読みやすいものになっています。
+
+```js:dist/main.js(抜粋)
+var num = +process.argv[2];
+console.log(fizzbuzz(num));
+function fizzbuzz(num) {
+ if (num % 15 == 0) {
+ return "FizzBuzz";
+ }
+ else if (num % 3 == 0) {
+ return "Fizz";
+ }
+ else if (num % 5 == 0) {
+ return "Buzz";
+ }
+ return num.toString();
+}
+```
+`prepare`は、`$ npm publish`する前などに自動的に実行されます。なので、公開時には自動的に本番の設定でビルドされたものがpublishされます。`$ npm run prepare`で手動実行することもできます。
+
+参考: [scripts \| npm Documentation](https://docs.npmjs.com/misc/scripts)
+
+### VSCodeでWatchする
+ここではVisual Studio Codeのnpm連携機能を使ってみます。
+
+コマンドパレットから`>Tasks: Configure Task`→`npm: watch`すると、下記JSONが生成されます。
+
+```json:tasks.json
+{
+ // See https://go.microsoft.com/fwlink/?LinkId=733558
+ // for the documentation about the tasks.json format
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "type": "npm",
+ "script": "watch",
+ "problemMatcher": []
+ }
+ ]
+}
+```
+コマンドパレットから`task npm: watch`すると、ターミナルウィンドウがひとつ自動的に立ち上がり`$ npm watch`を実行します。
+
+### npmignoreの設定
+ここではnpmライブラリとして公開したい場合を考えます。
+
+- gitリポジトリには、**`dist/`以下のビルド済JavaScriptファイルは無視**して、ビルド前のTypeScriptソースコードのみを登録したい
+- npmパッケージとしては、**`src/`以下のTypeScriptソースコードは無視**して、ビルド済のJavaScriptファイルのみを登録したい
+
+これらを実現するため、`.gitignore`に下記追記します
+
+```:.gitignore
+dist/
+```
+
+そして`.npmignore`を作成し、下記追記します
+
+```:.npmignore
+src/
+```
+すると、gitリポジトリにはTypeScriptコードのみが含まれ、npmパッケージにはビルド済JavaScriptファイルのみが含まれるようになります。
+
+参考: [developers \| npm Documentation](https://docs.npmjs.com/misc/developers#keeping-files-out-of-your-package)
+
+### 利用
+
+GitHubに上がっていれば、`npm install`コマンドでインストールすることが可能です。
+
+```
+$ npm install --global github_user_name/project_name
+```
+
+npmパッケージとして`$ npm publish`済であれば、下記コマンドでインストールできます。
+
+```
+$ npm install --global project_name
+```
+
+その他の詳細は、公式ドキュメントが詳しいです。
+https://docs.npmjs.com/cli/install
+
+## 参考
+
+- [React & Webpack · TypeScript](https://www.typescriptlang.org/docs/handbook/react-&-webpack.html)
+- [How to create and publish an npm module in TypeScript](https://codeburst.io/https-chidume-nnamdi-com-npm-module-in-typescript-12b3b22f0724)
+- [Node\.jsでCLIアプリ · JavaScriptの入門書 \#jsprimer](https://asciidwango.github.io/js-primer/use-case/nodecli/)