📦

pnpmとRollupでcli ツールを作成してnpmに公開する

2023/07/31に公開

最初に

文言と色を指定をしてコンソールで表示するだけの簡単なものを作ってみました。

https://www.npmjs.com/package/cli-tool-sample-rollup

ざっくり要件

  • cliツール用のnpmパッケージを作る
  • Typescriptで書きたい
  • サードパーティのパッケージを使いたい
  • コマンドライン時に引数を受け取れる

cliツールとしてパッケージ化するポイントは2つ

shebang書く

src/index.ts
#! /usr/bin/env node

シバンとは - 意味をわかりやすく - IT用語辞典 e-Words

package.jsonのbinフィールドでコマンド本体のファイルをマップする

package.json
"bin": {
    "print": "dist/index.js"
  },

そもそもの基本的なパッケージの作り方はこちらで書いています。
Viteのライブラリモードを使ってnpmパッケージを作成&公開してみる

pnpm でプロジェクト作成

pnpmをいれます。

https://pnpm.io/ja/installation

pnpm init

tsconfig の設定

tsconfig.json
{
  "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の設定

rollup.config.js
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-shebang.js
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を使ってコマンドライン引数を扱いやすくしてます。

src/index.ts
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で出力させる部分の処理

src/common/utils.ts
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}`)
}

カラーコードの定数

src/common/const.ts
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コマンドのコードになっています。

node_modules/.bin/print
#!/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も先頭に入っています。

node_modules/.bin/print
#!/usr/bin/env node
import t from"events";import e from"child_process";import i from"path";-省略-

スクリプトを追加します。

package.json
{
 ...
  "scripts": {
    "p": "print -w hoge -c fg"
  },
 ...
}

コマンドを実行します。

最後に

Typescript対応とサードパーティモジュールを使う部分で結構ハマってしまいました。最初Viteでやってみましたがいろいろ大変だったのと、余計なものも多い気がしたのでRollupだけでやるようにしました。

参考

Discussion