😇

npm を手探る

2022/11/08に公開

はじめに

この記事は、東京大学工学部電気電子工学科・電子情報工学科の実習「大規模ソフトウェアを手探る」のレポートとして書かれています。

ソースコード全体を把握することがほぼ不可能であるような大規模なオープンソースソフトウェアについて、所望の機能に関する部分を特定し、改良・機能拡張しよう、という趣旨の実習です。

この記事では、対象のソフトウェアとして npm を選択し、実際に「手探った」過程、成果を記述しています。

npmを手探り対象に決定

npmとは

npm (Node Package Manager)とはJavaScriptの実行環境の1つであるNode.jsのパッケージ管理システムです。

Node.js環境での開発において外部パッケージを使用する際、そのパッケージは他の複数のパッケージを用いて開発されていることがよくあります。npmの主な機能は、このようなパッケージの依存関係の解消であり、開発に使いたいパッケージをインストールすると、そのパッケージが依存する全てのパッケージを調べて自動でインストールしてくれます。

npmは、npmのレジストリ上に公開されている膨大な量のパッケージをサポートしており、Node.jsのパッケージマネージャーの代表格と言えます。

当初の目的

npmの主機能である 「パッケージの依存関係、競合関係を解消するメカニズムを明らかにしたい」 というモチベーションのもと、僕たちのチーム(kosei8,Mutekichi)は、この実験の題材としてnpmを選びました。

環境

  • 対象のnpm version : 9.0.0
  • node version : 18.10.0
  • JavaScript Debug Terminal

その1 : コマンド typo 時の仕様変更

背景

npm では、コマンド入力における多少のタイプミスは、自動で訂正して実行してくれます。

$ npm isntall

up to date, audited 1 package in 208ms

found 0 vulnerabilities

上の例では、 npm installnpm isntall とタイポしていますが、実行結果は npm install と正しく入力した場合と同じものになっています。

この例の他にも、例えば npm instnpm isnt のような入力に対しても npm install が実行されたり、 npm innitnpm init が実行されたりします。

変更目標

isntallinstall に補正されるのは良いと思ったのですが、 isntinstall になるのはやりすぎなんじゃないか、ということで、ひとまず npm isntnpm install が実行されないようにしてみたいと思います。

コードを読む

まず、メインとなる実行関数は ./bin/npm-cli.js にあるようなので、中身を確認すると

#!/usr/bin/env node
require('../lib/cli.js')(process)

のようになっていました。

lib ディレクトリ内の cli.js がエクスポートしている関数に対し、node.js のグローバル変数である process を引数にとって実行しますよ、という風になっています。ということで、 ./lib/cli.js に実質的にメインとなる関数が書かれていそうなので、その中身を見てみました。

./lib/cli.js でエクスポートされている関数はざっくり以下のようになっていました。

module.exports = async process => {
//中略
		await npm.exec(cmd, npm.argv)
    return exitHandler()
//中略
}

色々書いてあるのですが、 npm.exec という中枢感のある関数があるので、中身を調べてみます。

この関数は、 ./lib/npm.js 内の Npm というクラスのメソッドとして定義されています。関数内で、すぐに以下のような部分を見つけました。

async exec (cmd, args) {
  const command = await this.cmd(cmd)
//中略
}

例として、VScode の Terminal 上で、実行コマンドを

./{path-to-npm-src-dir}/bin/npm-cli.js isnt

としてデバッガを回してみたところ、上の時点で

cmd には 'isnt' が、 command には Install という名前のクラスのインスタンスが入っていることが確認できました。このことから、 this.cmd(cmd) の部分で、タイポの補正が行われていることが推察できます。

この cmd 関数は同じファイル内に定義されていて、

async cmd (cmd) {
//中略
  const command = this.deref(cmd)
//中略
  const Impl = require(`./commands/${command}.js`)
  const impl = new Impl(this)
  return impl
}

のようになっています。この部分では、まず、タイポを含んでいる可能性のあるコマンドの文字列 cmd ( 'isnt' )から、関数 deref により訂正後のコマンドの文字列 command ( 'install' ) を作っています。そこから、 Impl にコマンド毎に用意されたクラスの定義を呼び出し、 impl にそのクラスのインスタンスを作って返しています。

deref 関数を見てみると、中身に以下のような部分があります。

let a = cmdList.abbrevs[c]
    while (cmdList.aliases[a]) {
      a = cmdList.aliases[a]
    }
    return a

cmdList.aliases というのが如何にも目当ての部分な気がするので、中身を見てみると、

// ./lib/utils/cmd-list.js
//中略
const aliases = {
	//aliases
//中略
  //typos
//中略
  insta: 'install',
  instal: 'install',
  isnt: 'install',
  isnta: 'install',
  isntal: 'install',
//中略
}

ということで、ありました。タイポと正しいコマンドのマッピングリストを持っているようです。

変更結果

isnt: install の部分をコメントアウトして同じコマンドを実行してみると、

./{path-to-npm-src-dir}/bin/npm-cli.js isnt

Unknown command: "isnt"

To see a list of supported npm commands, run:
  npm help

のようになり、特定のタイポについて、自動でその補正をしないように変更することができました。

しかし、わざわざマッピングリストを作ってある通り、instinstall のエイリアスであるという意図を持って実装されているようです。installコマンドの公式のドキュメントにもエイリアスとして書いてありました。余計なことをしてしまったようです。。。

その2 : did you mean ~ ? 機能の仕様変更

背景

また、エイリアスにも引っかからない程のタイポをしてしまった際、正しいと思われるコマンドを提示してくれる場合があります。

$ npm verion
Unknown command: "verion"

Did you mean this?
    npm version # Bump a package version

To see a list of supported npm commands, run:
  npm help

上の例では、 npm version というコマンドのつもりで npm verion と入力してしまった際に、正しいと思われるコマンドとして npm version が表示されています。

変更目標

この機能について以下の変更を加えることにしました。

  • 正しいコマンドを表示してくれる入力ミスの範囲を広げる
  • y / n 入力でそのまま正しいコマンドを実行するか選択できるようにする

入力ミスの提案範囲拡張

コードを読む

一つ目の仕様変更である正しいコマンドを表示してくれる入力ミスの範囲を広げるという仕様変更については、すぐに行うことができました。

上でも登場した ./lib/cli.js には、以下のように、did you mean?機能に該当するのが一目瞭然な部分が含まれています。

//  ./lib/cli.js
	
	//中略
	  try {
	
	//中略、上述の await npm.exec(cmd, npm.argv) などを含む
	
	  } catch (err) {
	  if (err.code === 'EUNKNOWNCOMMAND') {
			const didYouMean = require('./utils/did-you-mean.js')
			const suggestions = await didYouMean(npm, npm.localPrefix, cmd)
	//中略
	}
}

ということで、 ./utils/did-you-mean.js の中身を見てみます。

//  ./utils/did-you-mean.js
const { distance } = require('fastest-levenshtein')
const readJson = require('read-package-json-fast')
const { commands } = require('./cmd-list.js')
//中略
const didYouMean = async(npm, path, scmd, rawArgs) => {
	const close = commands.filter(
		(cmd) => distance(scmd, cmd) < scmd.length * 0.4 && scmd !== cmd
	)
//中略
}

上で登場している distance という関数は、fastest-levenshtein というパッケージに定義されている関数で、ざっくり言うとレーベンシュタイン距離という文字列同士がどれだけ異なっているかの指標を返すものです。

これを踏まえると、 const close = 以降について

  • コマンドのリスト commands に対し、元のタイポを含みうるコマンドの文字列 scmd とコマンドリスト内の文字列 cmd の類似度を filter 関数を用いて調べている
  • 0.4 という数値が入力ミスの許容度を表すパラメータである

ということが容易に想像できます。

そこで、

//  ./utils/did-you-mean.js
//中略
const errorTolerance = 0.6 // default: 0.4
const didYouMean = async(npm, path, scmd, rawArgs) => {
	const close = commands.filter(
		手探る(cmd) => distance(scmd, cmd) < scmd.length * errTolerance && scmd !== cmd
	)
//中略、この内部にも 0.4 というパラメータを用いている箇所が存在したので、
//そこも errTolerance で置き換えた。
}

のようにして 0.4 と言うパラメータを 0.6 に置き換えました。

実行結果

  • 実行コマンド
{path-to-npm-src-dir}/bin/npm-cli.js hepl
  • 実行結果(変更前)
Unknown command: "hepl"

To see a list of supported npm commands, run:
  npm help
  • 実行結果(変更後)
Unknown command: "hepl"

Did you mean one of these?
    npm help # Get help on npm
    npm repo # Open package reopsitory page in the browser

To see a list of supported npm commands, run:
  npm help

hepl というタイポに対して、変更後では正しいと思われるコマンドを提示してくれるようになっており、「"hepl"なんて存在しないコマンドを入力するような君は"help"コマンドで勉強してこい」 という皮肉を言われずに済むようになりました。

y / n 実行機能追加

実装方針

元の did you mean ~ ? してくれる部分は以下のようになっています。

//  ./lib/cli.js
//前略
	if (err.code === 'EUNKNOWNCOMMAND') {
      const didYouMean = require('./utils/did-you-mean.js')
      const suggestions = await didYouMean(npm, npm.localPrefix, cmd)
      npm.output(`Unknown command: "${cmd}"${suggestions}\n`)
      npm.output('To see a list of supported npm commands, run:\n  npm help')
//後略

ここで、didYouMean 関数の返り値は、文字列として suggestions に入っています。

did you mean ~ ? とサジェストする内容がある場合には suggestionsdid you mean ~ ? のような文字列が入り、サジェストする内容がない場合には didYouMean 関数は空文字列を返してユーザには特にサジェストは表示されない、という仕組みになっています。

元の仕様だと、ユーザーからの入力を待つようなインタラクティブな仕組みが存在しないので、 didYouMean 関数が単に文字列を返すという実装方針で上手くいきますが、今実装している内容では、ユーザからの入力を待ち、その結果に応じて処理を変えるという仕組みが必要である以上、この実装方針では上手くいきません。

そこで、 didYouMean 関数が文字列を返すという仕様をやめて、 To see a list of … といったメッセージの出力も含めて、全て didYouMean 関数の中で行い、 didYouMean 関数自体は何も返さない、といった仕様に変更する必要があると考えました。

入出力関係

まず、JavaScript で入出力を取り扱う方法がわからなかったので調べてみたところ、readline というモジュールを用いる方法があるようです。

詳しくはこの記事に解説されており、僕たちのチームではこれを参考にしました。

問題点

y / n 機能を追加する上で悩んだのは、コマンド実行時のオプション ( -g など) や引数 (例えば npm install パッケージ名 の パッケージ名 の部分) をどのように扱うか、ということです。

例えば、 npm install -g パッケージ名 というコマンドを、間違えて

npm isntall -g パッケージ名

というように入力して実行しようとした場合を考えます( -g オプションは、パッケージをローカルインストールするのではなくグローバルインストールするようにするコマンドです)。

このとき、「npm install をこのまま実行しますか」と言う質問に yes で答えた場合に実行されるコマンドが、 npm install に相当するものであるべきか、あるいは npm install -g パッケージ名 に相当するものであるべきか、というのが悩ましいところです。

また、後者のようにする場合、オプションや引数をソースコード上でどのように取り扱うべきか、ということも悩みます。

僕たちのチームでは、オプションや引数を引き継ぐ後者の機能の方が実用的にはありがたいかな、というふうに考えたので、後者の実装方針で取り組むことにしました。

実装

まず、オプションや引数の情報がソースコード上ではどのように取り扱われているかについて調べてみることにしました。

typo 関連の部分で述べた通り、コマンドを具体的に実行している部分は、 ./lib/cli.js 内の

await npm.exec(cmd, npm.argv)

の部分です(より具体的には、この関数内で、文字列としてのコマンドからそのコマンドに対応するクラスのインスタンスが作られ、そのクラスに定義されている exec メソッドが各コマンドの内容を実際に

実行しています。)。

ここで、 npm isntall -g パッケージ名 というようなコマンドに対してデバッガを用いて調べてみたところ、予想では npm.argv の部分に -g と パッケージ名 の情報が入っていると思ったのですが、実際には npm.argv の部分には パッケージ名の情報しか入っていない、ということがわかりました。

cmd は単に文字列なので、ほかにオプションの情報が存在しうる部分が npm.execnpm の部分しかなさそう、ということでさらに詳しく調べたところ、やはりそのようでした。

デバッガで表示した npm の中身の一部。 -g をつけると global が true となり、つけないと false となる。

npm という変数には、巨大な Npm というクラス(その3で詳しく述べます)のインスタンスが代入されており、そのプロパティの global-g オプションの情報が保存されているようです。

ということで、 didYouMean 関数の中で、オプションの情報を保持している npm 変数を用いて

await npm.exec(cmd, args)

を実行し、 cmd の部分に実行コマンドの文字列を、 args の部分に引数の情報を入力すれば各種コマンドが実行できそうです。

変更後コード

以下のように変更を加えました。

//  ./lib/cli.js

//前略
} catch (err) {
    if (err.code === 'EUNKNOWNCOMMAND') {
      const didYouMean = require('./utils/did-you-mean.js')
      const rawArgs = process.argv.slice(3)
      npm.output(`Unknown command: "${cmd}"\n`)
      await didYouMean(npm, npm.loadPrefix, cmd, rawArgs)
      process.exitCode = 1
      return exitHandler()
    }
    return exitHandler(err)
  }
}
const { distance } = require('fastest-levenshtein')
const readJson = require('read-package-json-fast')
const { commands } = require('./cmd-list.js')

const readUserInput = (question) => {
  const readline = require('readline').createInterface({
    input: process.stdin,
    output: process.stdout,
  })

  return new Promise((resolve) => {
    readline.question(question, (answer) => {
      resolve(answer)
      readline.close()
    })
  })
}

const extractCommand = (extendedCommands, idx = 0) => {
  return extendedCommands[idx].split(' ')[5]
}

const didYouMean = async (npm, path, scmd, rawArgs) => {
  const errTolerance = 0.6 // default: 0.4
  // const cmd = await npm.cmd(str)
  const close = commands.filter(
    (cmd) => distance(scmd, cmd) < scmd.length * errTolerance && scmd !== cmd
  )
  let best = []
  for (const str of close) {
    const cmd = await npm.cmd(str)
    best.push(`    npm ${str} ${rawArgs.join(' ')} # ${cmd.description}`)
  }
  // We would already be suggesting this in `npm x` so omit them here
  const runScripts = ['stop', 'start', 'test', 'restart']
  try {
    const { bin, scripts } = await readJson(`${path}/package.json`)
    best = best.concat(
      Object.keys(scripts || {})
        .filter(
          (cmd) =>
            distance(scmd, cmd) < scmd.length * errTolerance &&
            !runScripts.includes(cmd)
        )
        .map((str) => `    npm run ${str} # run the "${str}" package script`),
      Object.keys(bin || {})
        .filter((cmd) => distance(scmd, cmd) < scmd.length * errTolerance)
        /* eslint-disable-next-line max-len */
        .map(
          (str) =>
            `   npm exec ${str} # run the "${str}" command from either this or a remote npm package`
        )
    )
  } catch (_) {
    // gracefully ignore not being in a folder w/ a package.json
  }

  if (best.length === 0) {
    return 0
  } else if (best.length === 1) {
    npm.output(
      `\n\nDid you mean this?\n --- npm ${extractCommand(
        best
      )}\n\nPress y to run:\n${best[0]}`
    )

    const inputChar = await readUserInput('')
    if (inputChar === 'y') {
      await npm.exec(extractCommand(best), npm.argv)
    } else {
      npm.output('To see a list of supported npm commands, run:\n  npm help')
    }
  } else {
    const numBest = best.length
    npm.output('\n\nDid you mean one of these?')
    for (let i = 0; i < numBest; ++i) {
      npm.output(` ${i + 1}.${best[i]}`)
    }
    npm.output(`\nPress 1 - ${numBest} to run one of them`)

    const inputNum = Number(await readUserInput(''))
    if (inputNum >= 0 && inputNum <= numBest) {
      await npm.exec(extractCommand(best, inputNum - 1), npm.argv)
    } else {
      npm.output('To see a list of supported npm commands, run:\n  npm help')
    }
  }
}
module.exports = didYouMean

変更箇所を全て網羅しようとすると長くなってしまったのですが、大まかな変更内容としては、

  • サジェストにオプションや引数まで含めて表示するために、 didYouMean 関数の引数に rawArgs を追加し、それをサジェスト部分に表示している(下例参照)
  • ユーザからの入力を受け付ける関数である readUserInput 、コマンドとその説明を含む文字列 best からコマンドの文字列そのものを取り出す関数 extractCommand を追加している
  • サジェストの数( best.length )が 2 以上である場合に、提示するコマンドのリストに番号を含めて表示するようにし、数字の入力を受け付けることで、コマンドを選択して実行できるようにしている

といった辺りです。

実行結果

{path-to-npm-src-dir}/bin/npm-cli.js nstal -g fastest-levenshtein
Unknown command: "nstal"

Did you mean one of these?
 1.    npm install -g fastest-levenshtein # Install a package
 2.    npm star -g fastest-levenshtein # Mark your favorite packages
 3.    npm unstar -g fastest-levenshtein # Remove an item from your favorite packages

Press 1 - 3 to run one of them
(⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂⠂): sill logfile start cleaning logs, removing 1 files

added 1 package, and audited 2 packages in 8s

found 0 vulnerabilities

上の例だと、入力ミスに対して、上の例だと、1 と入力することで、 npm install -g fastest-levenshtein と再入力することなく該当コマンドに相当する内容を実行することができています。

sill logfile … の部分は npm が元々持っているログ操作(のログ)なのですが、これが表示されてしまう問題は解決することができませんでした(エラー処理など細かいところをめちゃくちゃすっぽかしているあたりが原因か)。

1 という入力は上のログによって見えなくなっていますが、実際には 1 と入力しています。

その3 : ncu機能の実装

ncuとは

ncu (Npm Chech Updates) とはnpmにサポートされているパッケージの1つです。

ncuの主な機能は package.json に記載されているパッケージのバージョンが最新版であるかを調べるというものです。npmでインストールされたパッケージは package.json にパッケージ名とバージョンが記載されていますが、npm自身にはこのファイルを検証してパッケージのバージョン更新状況を調べる機能はありません。ncuはこの機能を提供してくれています。

この機能によって、全ての外部パッケージの更新状況を監視する必要がなくなります。また、ncuが package.json を書き換えることもできるので、パッケージのバージョン更新も手軽に行うことができます。

実装目標

1つのパッケージとしての機能のncuを、npmコマンドの1つとして、

npm ncu

のようなコマンドで実行できるようにすることを目標にしました。

このように機能をnpmに統一することで、開発の際にncuをインストールする手間が省けますし、ご意見様々あるでしょうが、npmの1つの機能としてncuがある方が自然な感じがします。

実装方針

ソースコードレベルで実装する

まず、ncuのソースコードをなんとかして、npmのソースコード内に組み込むことを考えました。

しかしこの方法は以下の理由により、限られた時間内では不可能であると見切りをつけました。

  • npmとncuのソフトウェアの設計が異なる
  • ncuはTypescript, npmはJavascriptで書かれている

npmとncuのソフトウェアの設計が異なる

これまでの過程でソースコードの大まかな全容が見えてきていました。

npmは Npm という巨大なクラスを定義し、そのメソッドとして全ての機能を実装しています。

// ./lib/npm.js
class Npm extends EventEmitter {
  static get version () {
    return pkg.version
  }
  command = null
  updateNotification = null
  loadErr = null
  argv = []

  #runId = new Date().toISOString().replace(/[.:]/g, '_')
  #loadPromise = null
  #tmpFolder = null
  #title = 'npm'
  #argvClean = []
  #chalk = null

  #logFile = new LogFile()
  #display = new Display()
  #timers = new Timers({
    start: 'npm',
  // 後略

上の画像は Npm クラス定義の導入部分ですが、このように大量のプロパティとメソッドがオブジェクトの階層構造を成して定義され、1つの巨大な Npm クラスにまとめられています。

Npm
|- commnd: object
|  |- description: string
|  |- name: sring
|  |- exec: method
|     |- globalTop: string
|     ............
|- logfile: object
|  |-................
|
|- cmd: method
|  |-...........
|- exec: method
|  |-...........
|
|-................

ごく一部を抜粋すると上のような構造になっていて、オブジェクトの階層構造の骨組みに、プロパティやメソッドが肉付けされていることが分かると思います。

また、メソッドは下の階層の要素のみに依存するというわけではなく、 Npm のどこかで定義されているプロパティやメソッドを参照しており、それぞれの要素が複雑に絡み合っています。

本題へ戻りますが、ncuをソースコードレベルでnpmに組み込もうとすると、この Npm クラスのフォーマットに従わなければ、うまく動作しないと考えられます。主に実行する箇所、ログファイルを書き出す箇所のncuのソースコードを変更し、npm仕様に変更しようと思ったのですが、やはり Npm クラスの複雑さと、両者の設計の違いから断念することにしました。

ncuはTypescript, npmはJavascriptで書かれている

断念したもう一つの理由としては両者の言語の違いがありました。

TypescriptはJavascriptのスーパーセットであるといえども、やはり様々な箇所で齟齬が起きてソースコードをそのまま扱うにはコストが高いと判断しました。

パッケージとして読み込んで扱う

実はnpmの開発自体にもnpmが使用されており、多くの外部パッケージが使用されています。

そこで、かなり無理矢理な方法ですが、ncuをソースコードで扱のではなく、npm開発フォルダの中にパッケージとしてインストールし、そのビルドされたファイルを扱うことでうまく Npm クラスの問題を回避できるのではないかと考えました。

この考えのもと、npmの処理の中で、ncuビルドされたコードの実行部分を外から強制的に叩くという手法を試すことにしました。(破茶滅茶なことをしていると理解していますが、機能としての実装を優先させました。)

まず、npm開発フォルダでncuをインストールします。

npm install npm-check-updates
// package.json
//  (前略)
    "node-gyp": "^9.1.0",
    "nopt": "^6.0.0",
    "npm-audit-report": "^3.0.0",
    "npm-check-updates": "^16.3.16",
    "npm-install-checks": "^5.0.0",
    "npm-package-arg": "^9.1.2",
//  (後略)

無事 package.json にncuが追加されました。

node_modules/npm-check-updates にあるファイルから実行部分を探すと、 npm-check-updates/build/src/cli.js というファイルが見つかります。

試しに、npmソースの外部のサンプルフォルダからこのファイルを実行すると、

./{path-to-npm-src-dir}/node_modules/npm-check-updates/build/src/cli.js

ncuを実行したときと同様の結果が得られました。

動作確認ができたので、ncuというコマンドが入力されたときに、この実行部分を叩けば良いだろうという淡い期待を抱いて、この方針で進むことにしました。

実装

実装のために以下の2点の変更を加えました。

  • lib/utils/comd-listnpm-check-updates を追加
  • ncu の commamd 型オブジェクト NpmCheckUpdates の作成

NpmCheckUpdates は以下のように、 exec description name のみの最低限のプロパティとメソッドを持ったものとして、簡易的に実装し、 exec ではモジュールとして入っているncuを呼び出すことのみ行っています。

// ./lib/commands/npm-ckeck-updates.js
const ncu = require('../../node_modules/npm-check-updates/build/src/bin/cli.js');
const BaseCommand = require('../base-command.js');
class NpmCheckUpdates extends BaseCommand {
    static description = 'Check out packages that can be updated';
    static name = 'npm-check-updates';

    async exec (args) {
        return await ncu();
    }
}
module.exports = NpmCheckUpdates

実行結果

実装が完了したので早速実行します。

./{path-to-npm-src-dir}/bin/npm-cli.js ncu

しかし、正常な結果もエラーも出力されませんでした。デバッガで検証すると、

このように実行部分まで進み、正常に処理を行っているように見えるのですが、何かがうまく行っていないようです。結局、原因を究明できず、実装は失敗に終わってしまいました。

原因考察

今回の実装失敗の原因として2つ挙げられると思います。

  • ncuの実行部分において標準入力の読み込み部分に齟齬
  • NpmCheckUpdates が簡易的なものであった

まず、1つ目の原因の根拠として、以下の2つのコマンドにおいて、上の動作確認のときのコマンドでは動き、下の実装のときのコマンドでは動かなかったことを考えます。両者の違いは引数があるかどうかの違いです。今回の実装ではnpmの入出力のみに書き換えを行い、ncuには手を加えていないので、ncuが標準入力を読み取るときに何らかの齟齬が生じている可能性が考えられます。

./{path-to-npm-src-dir}/node_modules/npm-check-updates/build/src/cli.js
./{path-to-npm-src-dir}/bin/npm-cli.js ncu

また、2つ目の原因の根拠として、何も出力されなかったことが挙げられます。

今回、簡素的に実装した NpmCheckUpdates には、エラー出力や ログ出力などの様々なメソッドが足りていません。足りていないことは分かるのですが、必要十分の境界が把握できなかったため、どこまで実装すれば良いか分かりませんでした。

Npm 型オブジェクトの全容を把握することは困難にしても、あるメソッド、プロパティの影響範囲を、根気よく解読していく作業が必要だったのかなと思いました。

まとめ

結果として npm の入出力周りを調べるだけに留まり、「パッケージの依存関係、競合関係を解消するメカニズムを明らかにしたい」 という目的まで辿り着けず、力不足を痛感しました。しかし、根気よくコードを読む作業を通じて、大規模ソフトウェアに対する抵抗が少し薄れた気がします。

今回は、小さく無益な変更しか出来ませんでしたが、いつかコントリビュータとして名を轟かせたいと思います。この記事を読んで下さった皆さんも、ぜひ興味のあるOSSを手探ってみてください。

参考記事

キーボードからの入力を取得する (reaqline.question) | まくまくNode.jsノート

そもそもnpmからわからない

Discussion