Object.create(null)

TypeError: Cannot convert object to primitive value

Node.js のパッケージ情報をいい感じに表示するやつを作った

ppp という, Node.js のパッケージ情報を表示するコマンドラインツールを作ったのでそのご紹介です.

github.com

それ npm view で良いじゃん, とか, 前も似たようなの作っていなかった? などと思った方もおられると思いますが, 新しく作ったのにはちゃんと理由がありますので, まずはお読みください...

背景

世の中には OS / プログラミング言語ごとに様々なパッケージマネージャがありますが, これらには大体 info サブコマンドのような機能があって, パッケージの情報を表示することができます. 例としていくつか挙げるとこんな感じです:

  • Homebrew
$ brew info node
node: stable 11.7.0 (bottled), HEAD
Platform built on V8 to build network applications
https://nodejs.org/
Not installed
From: https://github.com/Homebrew/homebrew-core/blob/master/Formula/node.rb
==> Dependencies
Build: pkg-config , python@2 
Required: icu4c 
==> Options
--HEAD
    Install HEAD version
==> Analytics
install: 289,885 (30 days), 959,391 (90 days), 3,944,852 (365 days)
install_on_request: 220,363 (30 days), 722,068 (90 days), 2,846,827 (365 days)
build_error: 0 (30 days)
  • Cabal
$ cabal info array
* array            (library)
    Synopsis:      Mutable and immutable arrays
    Versions available: 0.3.0.2, 0.3.0.3, 0.4.0.0, 0.4.0.1, 0.5.0.0, 0.5.1.0,
                        0.5.1.1, 0.5.2.0, 0.5.3.0 (and 4 others)
    Versions installed: 0.5.2.0
    Homepage:      [ Not specified ]
    Bug reports:   http://ghc.haskell.org/trac/ghc/newticket?component=libraries%20%28other%29&keywords=array
    Description:   In addition to providing the "Data.Array" module
                   <http://www.haskell.org/onlinereport/haskell2010/haskellch14.html
                   as specified in the Haskell 2010 Language Report>, this
                   package also defines the classes 'IArray' of immutable arrays
                   and 'MArray' of arrays mutable within appropriate monads, as
                   well as some instances of these classes.
    Category:      Data Structures
    License:       BSD3
    Maintainer:    libraries@haskell.org
    Source repo:   http://git.haskell.org/packages/array.git
    Dependencies:  base >=4.9 && <4.13
    Documentation: /usr/local/Cellar/ghc/8.4.4/share/doc/ghc-8.4.4/html/libraries/array-0.5.2.0
    Cached:        No
    Modules:
        Data.Array
        Data.Array.Base
        Data.Array.IArray
        Data.Array.IO
        Data.Array.IO.Internals
        Data.Array.IO.Safe
        Data.Array.MArray
        Data.Array.MArray.Safe
        Data.Array.ST
        Data.Array.ST.Safe
        Data.Array.Storable
        Data.Array.Storable.Internals
        Data.Array.Storable.Safe
        Data.Array.Unboxed
        Data.Array.Unsafe
  • opam
$ opam info base

<><> base: information on all versions ><><><><><><><><><><><><><><><><><><>  🐫 
name                   base
all-installed-versions v0.11.0 [4.06.1]
all-versions           v0.9.0  v0.9.1  v0.9.2  v0.9.3  v0.9.4  v0.10.0  v0.11.0  v0.11.1

<><> Version-specific details <><><><><><><><><><><><><><><><><><><><><><><>  🐫 
version       v0.11.1
repository    default
url.src:      "https://github.com/janestreet/base/releases/download/v0.11.1/base-v0.11.1.tbz"
url.checksum: "md5=e7e7dc5db3f1fea19d74a31bbd4ac621"
homepage:     "https://github.com/janestreet/base"
bug-reports:  "https://github.com/janestreet/base/issues"
dev-repo:     "git+https://github.com/janestreet/base.git"
authors:      "Jane Street Group, LLC <opensource@janestreet.com>"
maintainer:   "opensource@janestreet.com"
license:      "Apache-2.0"
depends:      "ocaml" {>= "4.04.1"}
              "sexplib0" {>= "v0.11" & < "v0.12"}
              "jbuilder" {build & >= "1.0+beta18.1"}
depopts:      "base-native-int63"
synopsis      Full standard library replacement for OCaml
description   Full standard library replacement for OCaml
              Base is a complete and portable alternative to the OCaml standard
              library. It provides all standard functionalities one would expect
              from a language standard library. It uses consistent conventions
              across all of its module.
              Base aims to be usable in any context. As a result system dependent
              features such as I/O are not offered by Base. They are instead
              provided by companion libraries such as stdio:
                https://github.com/janestreet/stdio

こういったコマンドがあるといちいちパッケージリポジトリを見に行ったりしなくて良いので大変便利ですね.

我らが npm には npm view (エイリアスとして npm infonpm show も使える) というサブコマンドがあります. ところがこれは一昔前はこんな出力をしていました1:

$ npm --version
5.8.0
$ npm view react-dom
 
{ name: 'react-dom',
  description: 'React package for working with the DOM.',
  'dist-tags': 
   { latest: '16.7.0',
     next: '16.8.0-alpha.1',
     canary: '0.0.0-3e15b1c69',
     tmp: '16.3.3',
     unstable: '0.0.0-0c756fb-f7f79fd' },
  versions: 
   [ '0.0.0-0c756fb-697f004',
     '0.0.0-0c756fb-f7f79fd',
     '0.0.0-3e15b1c69',
     '0.0.0-4a1072194',
     '0.0.0-7325ebe4d',
... (以下 500 行ほど続く)

JSONconsole.log しただけ, みたいな雑な感じなのはこの際許すとして, 平気で数百行単位で出力されるため, とても人間が読むことはできませんでした. (といっても頑張って読んだりしていた... またはいちいち npm view react-dom version みたいにして個々のフィールドを見ていた.)

これではあまりに不便に感じたため, 以前この npm view をラップして, いい感じに結果を整形して表示してくれるコマンド npm-sum を作りました.

susisu.hatenablog.com

これを使えばそこそこ見やすい形で出力してくれます:

$ npm-sum react-dom
  Name         : react-dom
  Version      : 16.7.0 (12/20/2018, 10:20:03 AM)
  Homepage     : https://reactjs.org/
  License      : MIT
  Description  : React package for working with the DOM.
  Keywords     : react
  Tags         :
    canary   : 0.0.0-3e15b1c69 (1/15/2019, 7:34:29 AM)
    latest   : 16.7.0 (12/20/2018, 10:20:03 AM)
    next     : 16.8.0-alpha.1 (1/16/2019, 8:22:20 AM)
    tmp      : 16.3.3 (8/2/2018, 3:58:58 AM)
    unstable : 0.0.0-0c756fb-f7f79fd (11/13/2018, 12:47:42 PM)
  Dependencies :
    loose-envify@^1.1.0, object-assign@^4.1.1, prop-types@^15.6.2,
    scheduler@^0.12.0

一方, 幸い現代では npm view も改良され2, 人間に読める形で表示されるようになりました.

$ npm --version
6.7.0
$ npm view react-dom

react-dom@16.7.0 | MIT | deps: 4 | versions: 135
React package for working with the DOM.
https://reactjs.org/

keywords: react

dist
.tarball: https://registry.npmjs.org/react-dom/-/react-dom-16.7.0.tgz
.shasum: a17b2a7ca89ee7390bc1ed5eb81783c7461748b8
.integrity: sha512-D0Ufv1ExCAmF38P2Uh1lwpminZFRXEINJe53zRAbm4KPwSyd6DY/uDoS0Blj9jvPpn1+wivKpZYc8aAAN/nAkg==
.unpackedSize: 4.5 MB

dependencies:
loose-envify: ^1.1.0  object-assign: ^4.1.1 prop-types: ^15.6.2   scheduler: ^0.12.0    

maintainers:
- acdlite <npm@andrewclark.io>
- brianvaughn <briandavidvaughn@gmail.com>
- fb <opensource+npm@fb.com>
- flarnie <flarnie.npm@gmail.com>
- gaearon <dan.abramov@gmail.com>
- sebmarkbage <sebastian@calyptus.eu>
- sophiebits <npm@sophiebits.com>
- threepointone <threepointone@gmail.com>
- trueadm <dg@domgan.com>
- zpao <paul@oshannessy.com>

dist-tags:
canary: 0.0.0-3e15b1c69          next: 16.8.0-alpha.1             unstable: 0.0.0-0c756fb-f7f79fd  
latest: 16.7.0                   tmp: 16.3.3                      

published a month ago by acdlite <npm@andrewclark.io>

では自作の npm-sum を捨てて npm view を使うようになったかと言うと, そうはなりませんでした. というのも以下のような点で不満があったためです.

  • 情報について:
    • dist.tarball や .shasum を見て使うのはパッケージマネージャであって, 人間ではないのでは?
    • dependencies を見て何か思うことはあまりなくて, むしろ peerDependencies の方が requirements なので知りたい
    • maintainers を並べられても特に嬉しいとは思わない
  • 色について:
    • deps: 4versions: 135 で数字の色が違うけれど, versions の方は黄色だから警告とかの意味があるのかな? と思うけど実は特に意味はない
    • 全体的にただカラフルなだけで, あまり整理されているような印象は受けず, 読みやすいとは思わない
  • フォーマットについて:
    • 見出し: 内容 のような構成なのかと思ったら例外も多かったり, 急に自然言語になったりして, 統一感がないように感じる
    • 最後に書いてある何時 publish されたかは割と知りたいし, 最初に書いてあるバージョンに関連した情報であって近くにある dist-tags の各バージョンとは関連しないので, どちらかといえば最初の方に書いてあって欲しい

こういった理由で npm-sum を使い続けていたわけですが, こちらも完璧な出来というわけではありません. 再度出力例を挙げますが,

$ npm-sum react-dom
  Name         : react-dom
  Version      : 16.7.0 (12/20/2018, 10:20:03 AM)
  Homepage     : https://reactjs.org/
  License      : MIT
  Description  : React package for working with the DOM.
  Keywords     : react
  Tags         :
    canary   : 0.0.0-3e15b1c69 (1/15/2019, 7:34:29 AM)
    latest   : 16.7.0 (12/20/2018, 10:20:03 AM)
    next     : 16.8.0-alpha.1 (1/16/2019, 8:22:20 AM)
    tmp      : 16.3.3 (8/2/2018, 3:58:58 AM)
    unstable : 0.0.0-0c756fb-f7f79fd (11/13/2018, 12:47:42 PM)
  Dependencies :
    loose-envify@^1.1.0, object-assign@^4.1.1, prop-types@^15.6.2,
    scheduler@^0.12.0
  • 情報の取捨選択ができていない:
    • とりあえず全ては生の JSON よりマシという気持ちで雑に作ったため, あまり取捨選択はしていなかった
    • 例えば npm view のところでも書いたように, dependencies よりも peerDependencies が表示されて欲しい
  • 知りたい情報の変化:
    • 例えば最近だと TypeScript 向けの型情報が提供されるかもわかると嬉しいと思う
  • 表示のカスタマイズ性:
    • ここまで この情報は知りたい とか 知りたくない とか書いているけれど, これは人によって違う可能性がある

といった課題が見つかってきます.

元々 npm-sum は「パッケージの情報を人間にも読めるように表示する」というコンセプトで作ったわけですが, npm view が人間にも読めるようになってしまった以上, この点については最早 npm-sum の強みではなくなってしまいました. ということで, ここまでに npm viewnpm-sum のそれぞれで挙げたような課題を解決し, 「パッケージの情報を適切に取捨選択した上で, 見やすい形で表示する」というコンセプトで新たなツールを作ることにしました.

ppp

完成したものがこちらです.

github.com

以下のコマンドでインストールできます.

npm i -g @susisu/ppp

使い方は以下のように npm view で取得した JSON を標準出力から受け取ります. これは npm-sum のように npm view をラップして動かす設計は責任が増えて筋が悪いなと思ったのと, 昨今のツールの多様化を考慮してです. 名前に npm を含まないのもそのため.

npm view react-dom --json | ppp

とはいえ面倒なので, 私は以下のようなスクリプトns という名前で作って ns react-dom みたいに使っています (標準で同梱しても良いかもしれない). ns が何の略なのかはご想像におまかせします.

#!/bin/sh

npm view "$@" --json | ppp

2019/02/03 追記: やっぱり ppp react-dom みたいに直接パッケージ名を指定しても動くようにしました. 利便性には勝てなかった.

閑話休題, デフォルトの設定では以下のような表示になります:

$ npm view react-dom --json | ppp
  Name              : react-dom
  Version           : 16.7.0 (12/20/2018, 10:20:03 AM)
  Description       : React package for working with the DOM.
  License           : MIT
  Homepage          : https://reactjs.org/
  Peer Dependencies : react@^16.0.0
  Tags              :
    canary   : 0.0.0-3e15b1c69 (1/15/2019, 7:34:29 AM)
    latest   : 16.7.0 (12/20/2018, 10:20:03 AM)
    next     : 16.8.0-alpha.1 (1/16/2019, 8:22:20 AM)
    tmp      : 16.3.3 (8/2/2018, 3:58:58 AM)
    unstable : 0.0.0-0c756fb-f7f79fd (11/13/2018, 12:47:42 PM)

デフォルト設定は見て分かる通りかなり表示項目を絞っていますが, 設定ファイルを使うことで表示項目をカスタマイズできるようになっています. 詳しくは README を参照してください.

私の場合は以下のように設定して, ES Modules や TypeScript 用の型定義の有無なども表示するようにしています.

fields:
  - name
  - version
  - description
  - license
  - author
  - homepage
  - npm
  - repository
  - module
  - types
  - binaries
  - engines
  - peerDependencies
  - tags
wrap: 120

こうしておくと, 例えば react-dom を TypeScript で使うときは別途型定義をインストール必要があるとか, 一方で rxjs の場合は必要ないといったことがわかって便利です.

$ npm view react-dom --json | ppp
  Name              : react-dom
  Version           : 16.7.0 (12/20/2018, 10:20:03 AM)
  Description       : React package for working with the DOM.
  License           : MIT
  Homepage          : https://reactjs.org/
  NPM               : https://www.npmjs.com/package/react-dom
  Repository        : git+https://github.com/facebook/react.git
  Module            : No
  Types             : No
  Peer Dependencies : react@^16.0.0
  Tags              :
    canary   : 0.0.0-3e15b1c69 (1/15/2019, 7:34:29 AM)
    latest   : 16.7.0 (12/20/2018, 10:20:03 AM)
    next     : 16.8.0-alpha.1 (1/16/2019, 8:22:20 AM)
    tmp      : 16.3.3 (8/2/2018, 3:58:58 AM)
    unstable : 0.0.0-0c756fb-f7f79fd (11/13/2018, 12:47:42 PM)
$ npm view rxjs --json | ppp
  Name        : rxjs
  Version     : 6.3.3 (9/26/2018, 8:52:58 AM)
  Description : Reactive Extensions for modern JavaScript
  License     : Apache-2.0
  Author      : Ben Lesh <ben@benlesh.com>
  Homepage    : https://github.com/ReactiveX/RxJS
  NPM         : https://www.npmjs.com/package/rxjs
  Repository  : git+https://github.com/reactivex/rxjs.git
  Module      : Yes
  Types       : Yes
  Engines     : npm@>=2.0.0
  Tags        :
    alpha          : 6.0.0-alpha.4 (3/14/2018, 4:00:55 AM)
    beta           : 6.0.0-beta.4 (3/30/2018, 5:15:32 AM)
    forward-compat : 5.6.0-forward-compat.5 (5/22/2018, 2:08:43 PM)
    latest         : 6.3.3 (9/26/2018, 8:52:58 AM)
    rc             : 6.0.0-uncanny-rc.7 (4/14/2018, 6:55:37 AM)
    smoosh         : 6.0.0-smoosh.1 (4/2/2018, 5:21:39 AM)
    unsmoosh       : 6.0.0-smoosh.2 (4/2/2018, 12:23:50 PM)

よかったですね〜.

おまけ

ところで Yarn にも yarn info というコマンドがありますが, こちらはどうでしょうか?

$ yarn info react-dom
yarn info v1.13.0
{ name:
   'react-dom',
  description:
   'React package for working with the DOM.',
  'dist-tags':
   { latest:
      '16.7.0',
     next:
      '16.8.0-alpha.1',
     canary:
      '0.0.0-3e15b1c69',
     tmp:
      '16.3.3',
     unstable:
... (以下 900 行ほど続く)

......


  1. 一応公平性のために言っておくと, 例として react-dom の情報を見ているのは後の話の都合上で, 私自身特に思い入れはないです.

  2. この当たり前のような機能を搭載した npm 5.10.0 がリリースされたのはなんと 2018 年 5 月 12 日 (より正確には 5.9.0-next.0 で 2018 年 3 月 24 日) で, なんとこの記事を書いている一年前の時点ではまだなかった

コマンドの出力を HTML に変換して貼り付ける

みなさんも生きていればコマンドの出力をブログに貼り付けたいということがあるでしょう. というわけでコマンド出力をなんとか HTML にして貼り付ける方法のご紹介です.

TL; DR: script -q /dev/null <command> | ansi2html -i | pbcopy


まず HTML は基本的には以下のような形で良いはずです. 色とかフォントとかは適当に変えたければ変える.

<pre style="color:#CCC;background-color:#000">
<!-- ここにコマンドの出力を貼り付ける -->
</pre>

例えば ls の結果を貼り付けたいとすると, 適当に pbcopy に pipe で流し込んでコピーすれば良さそうに思われます. この pbcopymacOS に標準で存在するコマンドで, 標準入力から受け取った内容をクリップボードにコピーします. 他の環境の場合は適当に置き換えて読んでください.

ls | pbcopy

これを貼り付けて完成. めでたし.

LICENSE
README.md
bin
coverage
jest.config.js
lib
node_modules
package.json
yarn-error.log
yarn.lock

で, 終わりではなくて, (ひょっとしたら環境によるかもしれないけれど) 普通に ls を実行したとき, こんな風に 1 行に 1 個のファイル / ディレクトリの形で出力されたでしょうか? おそらく以下のように, 1 行に複数個のファイル / ディレクトリが含まれる形になるのではないかと思います.

LICENSE     README.md   bin     coverage    jest.config.js  lib     node_modules    package.json    yarn-error.log  yarn.lock

なぜ出力が異なるかと言うと, コマンドは自身の出力が TTY 端末に対して行われているかを判別することができて, ls などコマンドによってはこれを見て出力を切り替えているのです.

例えば Node.js では以下のようなコードで判別できます. 出力が TTY 端末でない場合 (例えば pipe) に対して行われている場合は falsy な値 (undefined) が得られます.

$ node -e 'console.log(process.stdout.isTTY)'
true
$ node -e 'console.log(process.stdout.isTTY)' | cat
undefined

では TTY 端末に出力される結果をそのまま取得したい場合はどうしたら良いかというと, script コマンドを使ったトリックを使います. 例えば実行したいコマンドが foo bar とすると, 以下のように script 経由でコマンドを実行します.

# macOS の場合
script -q /dev/null foo bar
# Linux とかの場合
script --return -qfc /dev/null 'foo bar'

script は本来は対話的なコマンドの実行を記録するためのコマンドで, 通常は記録の出力先ファイルを指定するところを, ここでは記録は必要ないので /dev/null に捨てています.

重要なのは script 経由でコマンドを実行すると, たとえ script の出力が pipe などであったとしても, コマンド自身は TTY 端末に出力していると勘違いするような形で実行されるという点です. 実際, Node.js で判別するコードを使って試してみると, 次のような結果が得られます.

$ script -q /dev/null node -e 'console.log(process.stdout.isTTY)' | cat
true

ちなみにこの方法は昔色々ぐぐっていたところ bs-loader の PR で使われているのから見つけました.

さて元の話題に戻って, ls でやってみましょう.

script -q /dev/null ls | pbcopy
LICENSE     README.md   bin     coverage    jest.config.js  lib     node_modules    package.json    yarn-error.log  yarn.lock

ババーン.


ところで色付けたくないですか? ls には -G オプションがあり, これが有効な場合はファイル / ディレクトリや権限の違いなどがわかる形で色付けして出力してくれます. というわけでやってみましょう.

script -q /dev/null ls -G | pbcopy
LICENSE        README.md      [34mbin[39;49m[0m            [34mcoverage[39;49m[0m       jest.config.js [34mlib[39;49m[0m            [34mnode_modules[39;49m[0m   package.json   yarn-error.log yarn.lock

残念ながら, なんか [34m とか出てしまっています. これは ANSI escape code というやつで, この場合は要するに出力に色を付けるためのものが, 生のまま見えてしまっている形です.

これを HTML に変換してみましょう. 変換のためのツールは色々あるようですが, ちょっと調べた感じでは ansi2html というのが良い感じでした.

適当にインストールして,

pip install ansi2html

以下のように pbcopy の前に挟みます. -i は HTML の style をインラインの属性として出力するオプションで, -s solarized はカラースキーマを指定しています. 詳しくはansi2html --help を見ましょう.

script -q /dev/null ls -G | ansi2html -i -s solarized | pbcopy

結果はこんな感じ. 無事色がついてめでたいですね.

LICENSE        README.md      bin            coverage       jest.config.js lib            node_modules   package.json   yarn-error.log yarn.lock