pnpmとRollupでcli ツールを作成してnpmに公開する
最初に
文言と色を指定をしてコンソールで表示するだけの簡単なものを作ってみました。
ざっくり要件
- cliツール用のnpmパッケージを作る
- Typescriptで書きたい
- サードパーティのパッケージを使いたい
- コマンドライン時に引数を受け取れる
cliツールとしてパッケージ化するポイントは2つ
shebang書く
#! /usr/bin/env node
※ シバンとは - 意味をわかりやすく - IT用語辞典 e-Words
package.jsonのbinフィールドでコマンド本体のファイルをマップする
"bin": {
"print": "dist/index.js"
},
そもそもの基本的なパッケージの作り方はこちらで書いています。
Viteのライブラリモードを使ってnpmパッケージを作成&公開してみる
pnpm でプロジェクト作成
pnpmをいれます。
pnpm init
tsconfig の設定
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"emitDeclarationOnly": true,
"declaration": true,
"declarationDir": "./types",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
Rollupの設定
import terser from '@rollup/plugin-terser'
import typescript from '@rollup/plugin-typescript'
import { nodeResolve } from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import shebangPlugin from './rollup-plugin-shebang.js'
export default {
input: 'src/index.ts',
output: [
{
file: 'dist/index.js',
format: 'es',
plugins: [terser()],
},
],
plugins: [typescript(), nodeResolve(), commonjs(), shebangPlugin()],
}
プラグインについて
-
@rollup/plugin-terser
minifyようのプラグイン -
@rollup/plugin-typescript
rollupでtypescriptを使えるようにします。
tslibと相互依存なのでこれも一緒に入れます。 -
@rollup/plugin-node-resolve
Nodejsのアルゴリズムに従ってサードパーティのモジュールの場所を特定するためのプラグイン -
@rollup/plugin-commonjs
CommonJSモジュールをES6に変換するプラグイン
export default function shebangPlugin() {
return {
name: 'shebangPlugin',
renderChunk(code) {
return '#!/usr/bin/env node\n' + code
},
}
}
ビルドするとshebangが消されてしまうので、プラグインを使って#renderchunkのフェーズでコードの先頭に追加するようにしています。pnpmの場合はnode_modules/.bin/print
のコードがshコマンドのコードになっていて問題ないのですが、npmの場合はビルドしたjsのコードそのままなのでshebangがないと実行時にエラーになってしまいます。
コマンド用のコードを作成
メイン部分。
commanderを使ってコマンドライン引数を扱いやすくしてます。
import { program } from 'commander'
import { colorPrint } from './common/utils'
import { COLOR_MAP_KEYS } from './common/const'
;(() => {
program
.description('Display strings in console')
.option('-w, --word <char>', 'somthing display word')
.option('-c, --color <char>', `colorKey [${COLOR_MAP_KEYS.toString()}]`)
const parsed = program.parse(process.argv)
const args = parsed.opts()
colorPrint({ word: args.word, colorKey: args.color })
})()
console.logで出力させる部分の処理
import { PRINT_COLOR, PRINT_COLOR_MAP_ORIGIN, PRINT_COLOR_MAP } from './const'
type TPrintArgs = {
word?: string
colorKey?: keyof typeof PRINT_COLOR_MAP_ORIGIN
}
export const colorPrint = ({ word = 'hello', colorKey = 'fw' }: TPrintArgs) => {
const color = PRINT_COLOR_MAP.get(colorKey) ?? PRINT_COLOR.FG.WHITE
console.log(`${color}${word}${PRINT_COLOR.RESET}`)
}
カラーコードの定数
export const PRINT_COLOR = {
RESET: '\x1b[0m',
FG: {
BLACK: '\x1b[30m',
RED: '\x1b[31m',
GREEN: '\x1b[32m',
CYAN: '\x1b[36m',
WHITE: '\x1b[37m',
},
BG: {
BLACK: '\x1b[40m',
RED: '\x1b[41m',
GREEN: '\x1b[42m',
CYAN: '\x1b[46m',
WHITE: '\x1b[47m',
},
} as const
export const PRINT_COLOR_MAP_ORIGIN = {
fb: PRINT_COLOR.FG.BLACK,
fr: PRINT_COLOR.FG.RED,
fg: PRINT_COLOR.FG.GREEN,
fc: PRINT_COLOR.FG.CYAN,
fw: PRINT_COLOR.FG.WHITE,
bb: PRINT_COLOR.BG.BLACK,
br: PRINT_COLOR.BG.RED,
bg: PRINT_COLOR.BG.GREEN,
bc: PRINT_COLOR.BG.CYAN,
bw: PRINT_COLOR.BG.WHITE,
}
export const COLOR_MAP_KEYS = Object.keys(PRINT_COLOR_MAP_ORIGIN)
export const PRINT_COLOR_MAP = new Map(Object.entries(PRINT_COLOR_MAP_ORIGIN))
ビルド
cli-tool-sample-rollup $ pnpm build
> cli-tool-sample-rollup@1.0.3 build 省略/cli-tool-sample-rollup
> pnpm clean && rollup --config rollup.config.js
> cli-tool-sample-rollup@1.0.3 clean 省略/cli-tool-sample-rollup
> rimraf dist
src/index.ts → dist/index.js...
(!) Plugin typescript: @rollup/plugin-typescript TS5096: Option 'allowImportingTsExtensions' can only be used when either 'noEmit' or 'emitDeclarationOnly' is set.
created dist/index.js in 1.4s
ローカルで試す
pnpm link ./
を実行するとpackage.jsonのbinフィールドに基づいてシンボリックリンクが作成されます。
実行前 | 実行後 |
---|---|
node_modules/.bin
配下にprint
というコマンドが追加され、node_modules/cli-tool-sample-rollup
に現在のパッケージシへのンボリックリンクが作成されます。
これでコマンドが実行できるようになります。
ローカルで開発する際に挙動が確認できて便利です。
cli-tool-sample-rollup $ pnpm print
hello
npmに公開
ログイン
npm login
パブリッシュ
npm publish
pnpmのプロジェクトにインストールして試す
適当にプロジェクトを用意します。
pnpm init
pnpm i -D cli-tool-sample-rollup
インストールするとnode_modules/.bin/print
にシンボリックリンクが作成されます。
pnpmの場合はshコマンドのコードになっています。
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="省略/test-pnpm/node_modules/.pnpm/node_modules"
else
export NODE_PATH="省略/test-pnpm/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../cli-tool-sample-rollup/dist/index.js" "$@"
else
exec node "$basedir/../cli-tool-sample-rollup/dist/index.js" "$@"
fi
コマンドを実行します。
npmのプロジェクトにインストールして試す
適当にプロジェクトを用意します。
npm init
npm i --save-dev cli-tool-sample-rollup
インストールするとnode_modules/.bin/print
にシンボリックリンクが作成されます。
npmの場合はビルドしたjsのコードになっています。shebangも先頭に入っています。
#!/usr/bin/env node
import t from"events";import e from"child_process";import i from"path";-省略-
スクリプトを追加します。
{
...
"scripts": {
"p": "print -w hoge -c fg"
},
...
}
コマンドを実行します。
最後に
Typescript対応とサードパーティモジュールを使う部分で結構ハマってしまいました。最初Viteでやってみましたがいろいろ大変だったのと、余計なものも多い気がしたのでRollupだけでやるようにしました。
Discussion