ts-nodeで作成してたscriptをまとめる方法を模索した話

メインで書いている言語がjavasciptなので日頃、業務やらプライベートなりで使用するscriptをTypeScriptで書いているのですが
scriptを書くたびにpackage.jsonに追記していくのが非常に面倒だし、package.jsonのscriptsの中身の記述が増えすぎてえらいことになってきたので、整理できないかなと奮闘した記録になります。

package.json
```
scripts: {
  "script1": "./node_modules/.bin/ts-node src/scripts/script1.ts",
  "script2": "./node_modules/.bin/ts-node src/scripts/script2.ts",
  "script3": "./node_modules/.bin/ts-node src/scripts/script3.ts",
  "script4": "./node_modules/.bin/ts-node src/scripts/script4.ts"
　　// 作るたびに増えるし、毎回書くのは面倒くさい！！
}
```
#### やったこと
##### scriptをよびだすためのscriptを作成
各scriptを呼び出すためのscriptを書いてnpmコマンドの実行に応じて実行するscript名も受け取って呼べばpackage.jsonのscriptsは一つで済むことに気が付いたので、まずはそちらを作成しました。

scriptを呼び出すためのscript
```typescript
import { script1 } from './scripts/script1.ts';
import { script2 } from './scripts/script2.ts';

const scripts = {
  script1: script1,
  script2: script2
}

const executeScript = () => {
  const scriptName = process.argv[2];
  if (scripts[scriptName]) {
    scripts[scriptName]();
  } else {
    console.log('存在しないscript名です。現在使用可能なsctipt');
    Object.keys(methodMap).forEach(key => console.log(key));
  } 
};

try {
  executeScript();
} catch (e) {
  console.log(e);
}
```

```shell
npm run script script1
# script1が実行される
npm run script script3
存在しないscript名です。現在使用可能なsctipt
script1
script2
```
package.json
```diff
scripts: {
-  "script1": "./node_modules/.bin/ts-node src/scripts/script1.ts",
-  "script2": "./node_modules/.bin/ts-node src/scripts/script1.ts",
-  "script3": "./node_modules/.bin/ts-node src/scripts/script1.ts",
-  "script4": "./node_modules/.bin/ts-node src/scripts/script1.ts",
+  "script": "./node_modules/.bin/ts-node src/executeScript.ts"
}
```

これでpackage.jsonの記述がscriptを作るたびに増えていく問題が解決しました。
ですが依然として、作るたびに呼び出しを担っているscriptに記述していく面倒くささは残っています。

##### 作成したscriptをまとめる
次に考えたのはどうやればわざわざ手動でimportの追記していく手間を減らすかですが、
全くいい案が思いつかずscriptのファイル名をひろってimport文とまとめたobjectをつくってファイル書き出せば解決というスマートさのかけらもない手段を実行しました。

ファイルの操作やらディレクトリ内のファイルを取得などやりたいのでfsをインストール
```
npm i fs
```

強引にscriptを外部ファイルに吐き出すscript
```javascript
import * as fs from 'fs';

const basePath = './src/scripts/';
const importTemplate = `import { scriptName } from '../scripts/scriptName';`;
const exportTemplate = `export const scripts = {
scriptsObj
};`

/**
 * 作成したscriptのファイル名の取得.
 * @returns {Promise<Array<string>>}
 */
const getScripts = () => {
  return new Promise<Array<string>>((resolve) => {
    fs.readdir(basePath, (err, scrips) => {
      if (err) throw err;
      resolve(scrips.map((s) => s.split('.')[0]));
    });
  });
}

const createScriptsFile = async () => {
  const scripts: Array<string> = await getScripts();
  const [imports, scripts] = [[],[]];
  scripts.forEach((script) => {
    imports.push(importTemplate.replace(/scriptName/g, script));
    scripts.push(`  ${script}: ${script},`);
  });

  const exportStr = exportTemplate.replace('scriptsObj', scripts.join('\r\n'));
  fs.writeFile('src/const/scripts.ts', `${imports.join('\r\n')}\r\n${exportStr}`, 'utf8', (err) => {
    if (err) {
      console.log(err);
    } else {
      console.log('ファイルの作成に成功しました');
    }
  });
}

createScriptsFile();
```
めっちゃ強引ですが、実行すればscriptをまとめてくれるscriptが完成しました。
インデントとか合わせるのも強引にやっていますが、execを使ってファイルに対して`eslint --fix`とか書ければ
実行で作成されるファイル
```javascript
import { script1 } from '../scripts/script1';
import { script2 } from '../scripts/script2';

export const scripts = {
  script1: script1,
  script2: script2
}
```
作成されたファイルをscriptを呼び出すためのscriptにimportするように修正
```diff_javascript
- import { script1 } from './scripts/script1.ts';
- import { script2 } from './scripts/script2.ts';
+ import { scripts } from './constants/scripts';

- const scripts = {
-  script1: script1,
-  script2: script2
- }

// ~~ 変更はないので省略 ~~
```

これで毎回、追記はせずにcreateScriptsFileを実行すれば勝手に増えるようになってとても便利になりました。

##### 対話形式で実行するscriptを選択できるように修正
毎回、どこかのファイル対して追記を行う必要があったころに比べてかなり快適ですが、scriptのファイル名を覚えて一字一句間違えずに入力するのはなかなか面倒なので対話形式に変更します。

以前に業務で使って使いやすかったので今回はpromptsをモジュールを追加
```
npm i prompts
```

promptsを呼び出す処理を追記します。 

scriptを呼び出すためのscript
```diff_javascript
+ import * as prompts from 'prompts';
+ import { exit } from 'process';

+ /**
+  * 選択されたscript名の返却を行う.
+  * @returns {string} 選択されたscriptの名称.
+  */
+ const targetSelect = async () => {
+   const selectMenu = Object.keys(scripts).map((s) => ({  title: s, value: s }));
+   if (!selectMenu.length) {
+     console.log('実行するscriptが存在しません');
+     exit();
+   }
+   const questions = {
+     type: 'select',
+     name: 'target',
+     message: '実行するscriptを選択',
+     choices: selectMenu
+   };
+   const response = await prompts(questions);
+   return response.target;
+ }


- const executeScript = () => {
-  const scriptName = process.argv[2];
-  if (scripts[scriptName]) {
-    scripts[scriptName]();
-  } else {
-    console.log('存在しないscript名です。現在使用可能なsctipt');
-    Object.keys(methodMap).forEach(key => console.log(key));
-  }
- };
+ const executeScript = async () => {
+  const scriptName = await targetSelect();
+  scripts[scriptName](); 
+  };

try {
  executeScript();
} catch (e) {
  console.log(e);
}
```
script名を間違えることはなくなるので、存在確認していたif文を消して全体的にすっきりしました。
これで毎回固定のコマンドを実行するだけで済むので作業はかどります。

全く案が思いつきませんが、さらに改良を加えてscriptごとに引数を入力できるようなどをできるようにしたいですね