TypeScriptでnpmパッケージ（CommonJS・ES Moduleの両方に対応）を開発して公開してみた

## はじめに

普段から多くのOSSライブラリたちを利用させてもらっているが、そういったライブラリを自分でも作ってみたくなった。今回はあまり複雑ではないかなりシンプルなライブラリをTypeScriptで開発し、それを公開し利用するまでをやってみたいと思う。

開発するライブラリの機能としては、[Express](https://expressjs.com/)のmiddlewareになる。

## ライブラリを実装する

Expressのmiddlewareとして利用できるライブラリで、機能としてはAuthorizationヘッダーのBearerトークンを`req.token`から利用できるようにするもの。[cookie-parser](https://github.com/expressjs/cookie-parser/tree/master)や[body-parser](https://github.com/expressjs/body-parser/tree/master)のように、`req.○○○`のプロパティアクセスができるようにするもの。

今回はかなり単純に以下のような実装にした。

```ts:src/index.ts
import { Request, Response, NextFunction } from 'express';
import HttpError from './lib/http-error.js';

declare module 'express-serve-static-core' {
	interface Request {
		token: string | undefined;
	}
}

export interface BearerParserOptions {
	/**
	 * @default false
	 * @description If true, throw error when bearer token is invalid.
	 */
	isThrowError?: boolean;
}

const authBearerParser =
	(option?: BearerParserOptions) =>
	(req: Request, res: Response, next: NextFunction): void => {
		const { authorization } = req.headers;
		if (!authorization) {
			if (option?.isThrowError) throw new HttpError(401, `authorization header missing`);
			return next();
		}

		const [type, token] = authorization.split(/\s/, 2);
		if (type !== 'Bearer') {
			if (option?.isThrowError) throw new HttpError(400, `invalid token type: ${type}`);
			return next();
		}

		if (!token) {
			if (option?.isThrowError) throw new HttpError(401, `token missing`);
			return next();
		}

		req.token = token;
		return next();
	};

export default authBearerParser;
```
```ts:src/lib/http-error.ts
import { BaseError } from 'make-error';

export default class HttpError extends BaseError {
	status: number;

	constructor(status: number, msg: string) {
		super(msg);
		this.status = status;
	}
}
```

上記の実装で補足をする。

### 実装の補足
#### declare module 'express-serve-static-core' {...}
これは、`node_modules/@types/express-serve-static-core/index.d.ts`にある、以下の`interface Request {}`を拡張するための宣言。

```ts
declare global {
    namespace Express {
        // These open interfaces may be extended in an application-specific manner via declaration merging.
        // See for example method-override.d.ts (https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/method-override/index.d.ts)
        interface Request {}
        interface Response {}
        interface Locals {}
        interface Application {}
    }
}
```

まず前提として、Expressでは独自に`req`・`res`を拡張することができる（[Expressのerror handlingを理解し、middlewareで実装してみた](https://note.com/shift_tech/n/n42b96d36f0cf)や[Writing middleware for use in Express apps](https://expressjs.com/en/guide/writing-middleware.html)を参照）。JavaScriptであればどんなに独自拡張をしてもなにも困ることはないが、TypeScriptの場合、型で縛られるので`req.token`のようにもともと存在しないプロパティを設定する場合は、Expressの型を拡張する必要がある。

その型の拡張を行うために用意されているモジュールが`@types/express-serve-static-core/index.d.ts`であり、[Module Augmentation](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation)という方法でモジュールのinterfaceを拡張している（`namespace Express`に属する`interface Request`でも[Module Augmentation](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation)ができている認識だが、認識に誤りがあればご指摘ください）。

実際、コードのほうにも以下のように記載がされている通り、拡張するためにここに宣言されている事が分かる。

> These open interfaces may be extended in an application-specific manner via declaration merging.
（これらのオープン・インターフェイスは、宣言のマージによってアプリケーション固有の方法で拡張することができる。）

この拡張により、TypeScriptのほうで`interface Request`には`token`というプロパティがあることが認識でき、エラーにならなくなる。この設定がない場合、以下のようなエラーになる。
```console
$ npx tsc
src/index.ts:47:7 - error TS2339: Property 'token' does not exist on type 'Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>'.

47   req.token = token;
         ~~~~~


Found 1 error in src/index.ts:47
```

※ちなみに、今回はESLintでエラーになるのであえて以下のような実装はしなかったが、gobalで宣言されている`namespace Express`を拡張する方法でも同じ（[Global augmentation](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#global-augmentation)を参照）。
```ts
...
declare global {
	// eslint-disable-next-line @typescript-eslint/no-namespace
	namespace Express {
		interface Request {
			token: string | undefined;
		}
	}
}
...
```

- 参考：[How To Extend Express Request Interface in Typescript](https://plusreturn.com/blog/how-to-extend-express-request-interface-in-typescript/)

#### export default authBearerParser;

今回、default exportを使って実装したが、[なぜ default export を使うべきではないのか？](https://engineering.linecorp.com/ja/blog/you-dont-need-default-export)に書かれているように、named exportを利用する事で得られるメリットもある。その場合は、以下のような実装になるだろう。

```ts
import { Request, Response, NextFunction } from 'express';
import HttpError from './lib/http-error.js';

...

export interface BearerParserOptions {
	...
}

const authBearerParser =
	(option?: BearerParserOptions) =>
	(req: Request, res: Response, next: NextFunction): void => {
		...
	};

export { authBearerParser, BearerParserOptions };
```

## ローカル環境でパッケージを作成し、利用してテストする

上記まででいったんライブラリの実装は完了したので、npmにパッケージを公開する前に、実際に意図通りに利用できるか？の確認を行う。

以下のように`yarn pack`コマンドでローカル上でnpmパッケージを作成する事ができる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/bef3ae95-b40c-0712-b5da-d39710d3fc19.png)

上記を利用する他のプロジェクトを作成し、以下のようにpackage.jsonに依存を追加する。
```json:package.json
	"dependencies": {
		"express": "^4.18.2",
		"auth-bearer-parser": "file:/home/study/workspace/authorization-bearer-parser/auth-bearer-parser-v1.0.0.tgz"
	},
	"devDependencies": {
		...
		"@types/express": "^4.17.17",
        ...
	},
```

あとは作成したパッケージを利用するコードを実際に実装して、利用できるか？を確認するだけ。今回は以下のような実装を行い、実際にcurlでリクエストを送って`req.token`に値が入るか？を確認してみた。
```ts:src/index.ts
import express, { Request, Response } from 'express';
import authBearerParser from 'auth-bearer-parser';

const app = express();
app.use(express.json());
app.use(authBearerParser());

app.get('/', (req: Request, res: Response) => {
	console.log(req.token);
	res.send('Hello World!');
});

app.listen(3000, () => {
	console.log('Listening on port 3000!');
});
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/d22aae1d-2d2a-8837-15fe-803e5fdf61ea.png)
上記の画像のように、意図通り動作していることが確認できた。

- 参考：[Use npm pack to test your packages locally](https://dev.to/scooperdev/use-npm-pack-to-test-your-packages-locally-486e)
- 参考：[yarn pack](https://chore-update--yarnpkg.netlify.app/ja/docs/cli/pack)

### npmの場合

上記ではyarnでpackを行ったが、npmの場合は以下のようになる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/bdf2e488-5b20-3a98-cab3-54b1468ec445.png)

※上記はpackage.jsonの設定の`files`が以下のような状態での`npm pack`だったので、`esm`ディレクトリのみがパッケージに含まれている。
```json:package.json
{
	"type": "module",
	"types": "./esm/index.d.ts",
	"main": "./cjs/index.js",
	"module": "./esm/index.js",
	"exports": {
		".": {
			"import": {
				"types": "./esm/index.d.ts",
				"default": "./esm/index.js"
			},
			"require": {
				"types": "./cjs/index.d.ts",
				"default": "./cjs/index.js"
			},
			"default": "./cjs/index.js"
		}
	},
    ...
	"files": [
		"esm"
	],
}
```

## npmパッケージを公開するにあたっての準備をする

npmパッケージとして公開する前にpackage.jsonやREADMEなどに必要なことを記載する。このあたりはすでに公開されているライブラリ（パッケージ）の設定やREADMEを参考にして書く。

- 参考：[package.jsonの中身を理解する](https://qiita.com/dondoko-susumu/items/cf252bd6494412ed7847)

## npmパッケージを公開する

ここまでできたら、実際にnpmパッケージとして公開する。アカウントの取得から順番にやっていく。

### npmのアカウントを取得する

`npm adduser`コマンドでできる。コマンドを実行すると、URLが発行されるのでブラウザ上で新規にアカウントを作成すればいい。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/d7114a13-9eb4-4d78-d4e8-54a7f3ea7343.png)

ログインしているか？の確認は`npm whoami`で確認できる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/7ab58ccc-07f4-9cd8-4261-e0fb080e21a9.png)

### npmパッケージとして公開する

これは難しいことはなく、TypeScriptをコンパイルしてパッケージとして公開するファイルを生成してから、`npm publish`コマンドを実行するだけ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/9f142c61-e3aa-165f-5ae8-3480debad223.png)

## 公開したパッケージを利用してみる

```console
$ yarn add auth-bearer-parser
```

依存に追加してみて、ローカルでパッケージを作成した時と同じように検証をすると利用できることが確認できた。また、node_modules以下にちゃんと公開したパッケージのコードがあることを確認することもできた。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/36f9dcc0-042b-e7ac-3f82-b6818b471a5f.png)

## まとめとして

今回はTypeScriptでライブラリを実装して、それをnpmに公開するまでをやってみた。pure ESMで公開することも考えたが、[TypeScriptにおいてCommonJSとES Moduleの両方に対応するパッケージを作る](https://qiita.com/yuta-katayama-23/items/e72698698d96beb6e002)で取り上げたようなデュアルパッケージ（CommonJSとES Moduleの両方で利用できる）として公開することにした。

以下、実際に公開したパッケージ。

https://www.npmjs.com/package/auth-bearer-parser

GitHubは以下。

https://github.com/yutak23/auth-bearer-parser

## おまけ
### GitHubのページでプロっぽさを出す

以下のように、Releasesがあるとなんかそれらしい感じになるので、これをやってみる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/7306db92-2d0b-6321-a8f2-7bbc0b897569.png)

やり方は簡単で`git tag`でタグを作成して、GitHub上でReleaseを作成するだけ。
```console
$ git tag -a v1.0.1 -m "version 1.0.1" 

$ git push origin v1.0.1
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 161 bytes | 161.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0
To github.com:yutak23/auth-bearer-parser.git
 * [new tag]         v1.0.1 -> v1.0.1
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/dbacaef5-f881-1719-d553-ab0e383972c2.png)

- 参考:[git tag と GitHub の Release 機能でプロっぽさを出してみよう](https://qiita.com/tommy_aka_jps/items/5b39e4b27364c759aa53)

### READMEにバッヂを表示する

以下のようなやつ。
![test](https://github.com/yutak23/auth-bearer-parser/actions/workflows/test.yaml/badge.svg)

これは
```
![test](https://github.com/yutak23/auth-bearer-parser/actions/workflows/test.yaml/badge.svg)
```
のようにすれば表示できる。

- 参考：[Adding a workflow status badge](https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/adding-a-workflow-status-badge)