npmパッケージをScope付きで公開してみよう！

## 内容

以下の仕様のCLIプログラムを`NPM`に公開してみます。

## GitHub Repository

本記事で作成するPackageは[GitHub](https://github.com/HIppei/test-pair-checker)で公開しています。

## 仕様

プログラム名は、`test-pair-checker`とします。

**テスト対象ファイル**に対応する**テストファイル**を検索し、存在有無をチェックします。これにより、ファイルの存在有無レベルですが、簡単にテスト不足を検知できます。

以下の構成の場合、`src/components/custom-button.tsx`は対応するテストファイルが**存在する**、`src/components/custom-input.tsx`は対応するテストファイルが**存在しない**と検知できればOKとなります。

```bash:Exapmle
.
├── src
│   ├── components
│       ├── custom-button.tsx
│       ├── custom-input.tsx
│   ├── tests
│       ├── components
│           ├── custom-button.test.tsx
```

### 入力
`test-pair-checker.config.js`ファイルに以下の入力を準備。
・テスト対象フォルダ(string[])
・除外するフォルダパターン(string[])
・テストフォルダ(string)

### ファイル同士の照会
`config`ファイルからから、`*.test.*`が存在するかチェックする。
そして下記記載の出力を行う。

### 出力

```bash:shell
------------------------------------------------------
The number of test target files 対象ファイル数
The number of test files テストファイル数
The number of matched test files 対応するテストファイル数
------------------------------------------------------
You must implement corresponding test files for the following files.
 [ テストファイルが存在しない対象ファイル一覧 ]
```

### 使用方法
・CLI

## プロジェクト作成

```bash:shell
# 既にnpm公開済みの名称なので、もしテスト公開したい場合は以下の名前以外を付けてください。
$ mkdir test-pair-checker && cd ./test-pair-cherk
$ npm init -y

# srcフォルダ配下にコードを記載していきます。
$ mkdir src
```

※Package Manageはpnpmで記載しておりますが、こちらも適宜、`npm`や`yarn`で読み替えてください。

## 実装

以下の順番で進めていきます。
シンプルに公開までさっさと進めたいという方は、`*`がついている手順をスキップしてください。

1. [Packageファイル出力]()
`dist`フォルダ配下にPackageファイルを出力することを目指します。
バンドラーには`Rollup`、コーディングは`TypeScript`を使用します。
Rollupを選択した理由は、[この記事](https://medium.com/webpack/webpack-and-rollup-the-same-but-different-a41ad427058c)の***Use webpack for apps, and Rollup for libraries***という記載を見かけたのと、最小限の構成からスタートし、必要分だけ`Plugin`を追加していくという方式が分かりやすかったためです。

1. ローカルテスト
NPM Packageに公開してから、テストをおこなっていては効率、信頼性がともに損なわれます。
便利なPackage manageのlink機能を使い、ローカルテストで正常性を確認します。
    
1. *Linter、Formatter、Testing
継続性のある開発を行えるように設定を行います。

1. Description
READMEに使用方法などを記載します。

1. NPMにPackageを公開 
`npm login`を行い、Packageを公開します。

<br/>

### 1.Packageファイル出力

必要なPackageをインストールします。

RollupのPluginは実装内容に応じてというのが前提ではありますが、
今回使用しているものはよく使う部類に入るので、知っておいて損はないと思います。

```bash:shell
# 開発時に使用するので、devDependenciesに入れます。
$ pnpm add -D \
# Rollup バンドラー本体です。
rollup \

# TypeScriptをコンパイルする際に使用します。
typescript \

# TypeScriptファイルをバンドルする際に使用します。
# typescriptとtslibはpeer dependenciesにも記載されており、必須パッケージとなります。
@rollup/plugin-typescript \
tslib \

# サードパーティ製（Package.jsonのdependencies、peerDependencies、nodeのビルトインモジュール）をそのままパッケージとして保持してくれます。
# 実際にライブラリをインストールして使うときに、本体プロジェクト側で解決されることを前提とした仕組みです。
rollup-plugin-node-externals \

# コメントや空白などの無駄な行を削除します。
@rollup/plugin-terser \

# processを使用するためです。
@types/node
```

また、ファイル検索のコアとなるモジュールをインストールします。

```bash:shell
# globパッケージはライブラリインストール時に、本体プロジェクト側でインストールされます。
$ pnpm add glob
```

- `package.json`を編集

特に、NPM Packageに関連してくる内容を説明します。

`name`:`your-scope`に関しては、`NPM`アカウント取得後に確定するので一旦は何でもOKです。これがインストール時の名称となります。

`version`:Packageの初版Verです。完成版ではないので、`0.9.0`にしています。

`main`:例えば、test-pair-checkerフォルダ直下で、`node ./`とコマンドしたときに、記載されているファイルが実行されます。

`type`:ESMモジュールを前提とします。`commonjs`の記載を行う場合は、拡張子に`cjs`が必要となります。

`bin`:このPackageをインストール後に、`pnpm pair-check`というコマンドで、指定されたファイルが実行されます。CLIプログラムの重要なポイントです。

`files`:Packageインストールに付帯するファイルを指定します。`package.json`はデフォルトで含まれます。


完成形は以下です。

```json:/package.json
{
  "name": "@your-scope/your-file-name",
  "version": "0.9.0",
  "description": "This checks corresponding test file existance based on those names.",
  "main": "./dist/bin/index.js",
  "type": "module",
  "bin": {
    "pair-check": "./dist/bin/index.js"
  },
  "files": [
    "dist"
  ],
  "scripts": {
    "build": "rollup -c -w"
  },
  "author": "Your name",
  "license": "MIT",
  "dependencies": {
    "glob": "^10.3.10"
  },
  "devDependencies": {
    "@rollup/plugin-terser": "^0.4.4",
    "@rollup/plugin-typescript": "^11.1.5",
    "@types/node": "^20.10.4",
    "rollup": "^4.7.0",
    "rollup-plugin-node-externals": "^6.1.2",
    "tslib": "^2.6.2",
    "typescript": "^5.3.3"
  }
}
```

- `tsconfig.json`を追加

```json:tsconfig.json
{
  "compilerOptions": {
    "esModuleInterop": true
  },
  "include": [
    "src"
  ],
  "exclude": [
    "node_modules",
    "dist"
  ]
}
```

- コアロジック

以下より、コピーしてください。

[src/index.ts](https://github.com/HIppei/test-pair-checker/blob/main/src/index.ts)

[src/matcher.ts](https://github.com/HIppei/test-pair-checker/blob/main/src/matcher.ts)

[src/utils.ts](https://github.com/HIppei/test-pair-checker/blob/main/src/utils.ts)

また、デフォルト値として取得される`config`ファイルを作成します。
※このPackageのインストールを行うプロジェクトのRootフォルダに`test-pair-checker.config.{js,cjs}`が存在する場合は、使用されません。

```javascript:src/test-pair-checker.config.js
const config = {
  targetDirs: ['src'],
  ignorePatterns: [],
  testDir: 'tests',
};

export default config;
```

- Rollup設定

```javascript:/rollup.config.js
import typescript from '@rollup/plugin-typescript';
import nodeExternals from 'rollup-plugin-node-externals';
import terser from '@rollup/plugin-terser';

const plugins = [
  // This helps Rollup understands typescript.
  typescript(),

  // This excludes packages under peer-dep, dep and built-in node modules.
  nodeExternals(),

  // This minifies bundled file to eliminate empty line and comments.
  terser({ format: { comments: false } }),
];

/** @type {import('rollup').RollupOptions} */
const cliConfig = {
  input: 'src/index.ts',
  output: {
    dir: 'dist/bin',
    strict: true,
  },
  plugins,
};

export default cliConfig;
```

- Packageファイルを出力

`dist/bin/index.js`にバンドルファイルが生成されていればOKです。

```bash:shell
$ pnpm build

rollup v4.7.0
bundles src/index.ts → dist/bin...
created dist/bin in 743ms

[xxxx-xx-xx xx:xx:xx] waiting for changes...
```

<br/>

### 2.ローカルテスト

- ローカルで公開

`npm`、`yarn`も同様の方法があるので、適宜調整してください。

`test-pair-checker`配下で以下のコマンドを実行します。

```bash:shell
$ pnpm link -g

WARN  xxx/test-pair-checker has no binaries
xxx/.local/share/pnpm/global/5:
+ @[your-scope]/[your-file-name] 0.9.0 <- ../../../../../xxx/test-pair-checker
```

- インストールを行うテストプロジェクトを作成

```bash:shell
# test-pair-checkerとは別フォルダに移動し、新プロジェクトを生成
$ mkdir package-install
$ cd package-install
$ npm init -y
```

`config`ファイルを作成します。拡張子は`cjs`です。
※簡単のため`cjs`にしています。`js`にしたい場合は、`package-install`の`package.json`に`"type":"module"`を追加してください。

```javascript:/test-pair-checker.config.cjs
module.exports = {
  targetDirs: ['src'],
  ignorePatterns: [],
  testDir: 'tests',
};
```
テストを行えるように、テスト対象ファイルとテストファイルを適当に作成します。以下のようなフォルダ構成になるようにファイルを生成してください。中身は空でOKです。

```bash
.
├── src
│   ├── components
│       ├── custom-button.tsx
│       ├── custom-input.tsx
│   ├── tests
│       ├── components
│           ├── custom-button.test.tsx
├── dummy
│   ├── components
│       ├── dummy-button.tsx
│
├── test-pair-checker.config.cjs
├── package.json
```

- ローカル公開されたPackageをインストール

`package-install`配下で以下のコマンドを実行します。

すると、`node_modules`配下にインストールされます。
※正確にはSymlinkが張られた状態となります。

```bash:shell
$ pnpm link -g @[your-scope]/[your-file-name]

xxx/.local/share/pnpm/global/5:
+ @[your-scope]/[your-file-name] 0.9.0 <- node_modules/@[your-scope]/[your-file-name]
```

- `pair-check`コマンド

テスト対象ファイルが2つ見つかり、そのうち`custom-button.tsx`はテストファイルが存在し、`custom-input.tsx`には存在していないことが分かります。また、対象外の`dummy`フォルダからは特に影響を受けていないことが分かります。

```bash:shell
$ pnpm pair-check

------------------------------------------------------
The number of test target files 2
The number of test files 3
The number of matched test files 1
------------------------------------------------------
You must implement corresponding test files for the following files.
 custom-input.tsx
```

ローカルテストはOKとします。

<br/>

### 3.*Linter、Formatter、Testing

継続的な開発に欠かせないPackageを導入していきます。

- Linter

以下のコマンドでインストールします。基本的なルールが導入された`.eslintrc.cjs`が生成されますので、それをそのまま使用します。`package.json`には、`eslint`用のスクリプトを追加します。



```bash:shell
$ npm init @eslint/config

Need to install the following packages:
@eslint/create-config@0.4.6
Ok to proceed? (y) 
✔ How would you like to use ESLint? · problems
✔ What type of modules does your project use? · esm
✔ Which framework does your project use? · none
✔ Does your project use TypeScript? · No / Yes
✔ Where does your code run? · browser
✔ What format do you want your config file to be in? · JavaScript
Local ESLint installation not found.
The config that you've selected requires the following dependencies:

@typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest eslint@latest
✔ Would you like to install them now? · No / Yes
✔ Which package manager do you want to use? · pnpm
```

```json:/package.json
 "scripts": {
    "build": "rollup -c -w",
    "lint": "eslint --ext js,jsx,ts,tsx src",
  },
```

`eslint`を起動し、問題なければOKです。

```bash:shell
$ pnpm lint

> @[your-scope]/[your-file-name]@0.9.0 lint /xxx/test-pair-checker
> eslint --ext js,jsx,ts,tsx src
```

- Formatter

`Prettier`を導入します。`prettierrc.config.js`も設定します。

```bash:shell
$ pnpm add -D -E prettier
```

```javascript:/prettierrc.config.js
/** @type {import("prettier").Config} */
const config = {
  printWidth: 120,
  trailingComma: "es5",
  tabWidth: 2,
  semi: true,
  singleQuote: true,
  endOfLine: "lf",
};

export default config;
```

- `husky`と`lint-staged`

これらを使用し、`Git Commit`時に`staging`されたファイルに対し、`eslint`と`prettier`を実行します。これにより、ソース保全を強制させることができます。

関連Packageをインストール後、ファイルを調整します。

```bash:shell
# husky
$ pnpm dlx husky-init && pnpm install

# lint-staged
$ pnpm add -D lint-staged
```

```bash:/.husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

pnpm lint-staged
```

```json:/package.json
  "scripts": {
    "build": "rollup -c -w",
    "lint": "eslint --ext js,jsx,ts,tsx src",
    "lint-staged": "lint-staged",
    "prepare": "husky install"
  },
  ,
  ,
  ,
  "lint-staged": {
    "*.*": "npx prettier --write",
    "*.{js,cjs,ts}": "npx eslint"
  }
```

動作チェックしてみます。以下のように、不使用の`const test`を定義し、Commitをしようとすると、、

```typescript:/src/index.ts
import { globSync } from 'glob';
import matcher from './matcher';
import { cwd } from 'process';

const test = 'test';

// 省略
```

以下のようにエラーが発生し、`git commit`が失敗します。

```bash:shell
$ git commit -a -m "test commit"

> @[your-scope]/[your-file-name]@0.9.0 lint-staged /xxx/test-pair-checker
> lint-staged

✔ Preparing lint-staged...
⚠ Running tasks for staged files...
  ❯ package.json — 2 files
    ✔ *.* — 2 files
    ❯ *.{js,cjs,ts} — 1 file
      ✖ npx eslint [FAILED]
↓ Skipped because of errors from tasks.
✔ Reverting to original state because of errors...
✔ Cleaning up temporary files...

✖ npx eslint:

/xxx/test-pair-checker/src/index.ts
  5:7  error  'test' is assigned a value but never used  @typescript-eslint/no-unused-vars

✖ 1 problem (1 error, 0 warnings)

 ELIFECYCLE  Command failed with exit code 1.
```

- Testing

NPM Packageの信頼性に大きく寄与するUnitテストを導入します。フレームワークは`jest`を使用します。

`typescript`に対応するためのPackageも導入します。

```bash:script
$ pnpm add -D jest @types/jest ts-jest
```

テスト詳細は、[Repo](https://github.com/HIppei/test-pair-checker/tree/main/tests)を確認ください。

<br/>

### 4.Description

`README.md`にPackage詳細を記載していきます。

[Repo](##GitHubRepository)を確認ください。

<br/>

### 5.NPMにPackageを公開

- アカウント作成

[NPM](https://www.npmjs.com/signup)でアカウントを作成します。

作成した**アカウント名がScope**となります。これまで`@[your-scope]`としてきたところに格納され、私の場合は以下のようになります。

```json:package.json
{
    "name":"@i2i3i/test-pair-checker"
    ,
    ,
}
```

- `npm login`

```bash
$ npm login

Login at:
https://www.npmjs.com/login?next=/login/cli/xxxxx
Press ENTER to open in the browser...

## Browserでログインが完了すると、、
Logged in on https://registry.npmjs.org/.
```

- 公開

公開対象のPackage配下で以下のコマンドを実行します。

```bash
$ npm publish --access public
```

[こちら](https://www.npmjs.com/package/@i2i3i/test-pair-checker)に公開されています。

## 最後に

想定よりも大幅に時間がかかりました。OSS活動をされている方や、NPM Packageを公開されている方達は本当に凄いです。

ただ、今回の記事のように、普通の開発者がちょっとした便利ツールを公開することにも使えるので、どんどん作ってみると良いですね。

興味がある方は、ぜひインストールしてみてください。


## 参考

[eslint](https://eslint.org/docs/latest/use/getting-started#quick-start)
[prettier](https://prettier.io/docs/en/install.html)
[husky](https://typicode.github.io/husky/getting-started.html#automatic-recommended)
[lint-staged](https://github.com/lint-staged/lint-staged?tab=readme-ov-file#-lint-staged----)