自作npmパッケージの公開

自作npmパッケージを作成したので、公開/バージョンアップ/削除の方法をまとめておきます。

ちなみに作成したものは以下です。

www.npmjs.com

npmパッケージの公開方法

1. プロジェクトのセットアップ
  • プロジェクトフォルダの作成
  • package.jsonに必要な情報(名前、バージョン、説明等)を設定する。

    • 公開するnpmパッケージにはpackage.jsonが含まれている必要がある。
      プロジェクトのルートディレクトリでnpm initコマンドを実行し、プロンプトに表示された各項目の質問に答えるとpackage.jsonが作成される。
      【参考】
      Creating a package.json file
  • package.jsonbin項目を追加し、公開したパッケージをnpx 自作パッケージ名で使用できる。

"bin": {
  "自作パッケージ名": "./index.js"
},
2. 依存パッケージの追加
  • 依存パッケージをpackage.jsonの以下に追加する。

    • "dependencies":必要な外部ライブラリやパッケージなど、アプリケーションの実行に必要なものを定義。
    • "devDependencies":外部ライブラリやパッケージなど、ローカルでの開発のみに必要なものを定義。
  • 必要な依存パッケージを追加するため、
    プロジェクトのルートディレクトリでnpm installを実行。

# "dependencies"に追加
$ npm install <パッケージ名>

# "devDependencies"に追加
$ npm install <パッケージ名> --save-dev

【参考】
Specifying dependencies and devDependencies in a package.json file

3. ローカルでのリンク作成(オプション)
  • プロジェクトのディレクトリでnpm linkを実行。
    他のローカルプロジェクトでこのパッケージを利用できるようになる。
4. アカウント作成とログイン
  • npmの公式でアカウント作成。
  • ターミナルでnpm loginと入力し、ログイン。
5. 公開

npm publishを実行し、エラーがなければ、npmリポジトリにパッケージが公開される。

npmパッケージのバージョンアップ方法

1. バージョン更新
  • 公開されているパッケージに変更を加えた場合、自分のコードに依存している他の人に変更を伝えるために、バージョン番号を更新する必要がある。
# メジャーバージョンアップ: 大きな変更があって、前のバージョンと互換性がなくなる場合。新しい主要機能の追加など。
$ npm version major
v2.0.0

# マイナーバージョンアップ: 以前のバージョンとの互換性を保ちつつ、新しい機能を追加する場合。中程度の改善や機能追加など。
$ npm version minor
v1.1.0

# パッチバージョンアップ: 小さなバグ修正や、既存の機能の軽微な改善を行う場合。
$ npm version patch
v1.0.1
2. 変更のコミット
  • npm-versionは更新したバージョン番号のコミットとtagも同時に生成。 リモートレポジトリに最新のコミットとタグを反映。
3. 再公開

ターミナルでnpm publishコマンドを再度実行。 新しいバージョンがnpmリポジトリに公開される。

【参考】
Updating your published package version number
npm-version
2.6 Git の基本 - タグ

npmパッケージの削除方法

1. 最近公開されたバージョンの削除
  • 特定バージョンを削除する場合、ターミナルで以下のコマンドを使用。
$ npm unpublish <パッケージ名>@<バージョン番号>
2. 全バージョンの削除
$ npm unpublish <パッケージ名> -f

注意点:

  • 一度削除すると、同じバージョン番号での再公開はできない。
  • 72時間の制限は、誤ったバージョンを削除できるようにするためのもの。単一バージョンの削除はこの期間に限定される。
  • 完全削除後、同じ名前で再公開する場合は24時間待つ必要がある。

【参考】
Unpublishing packages from the registry
npm Unpublish Policy
npm-unpublish

カレンダーのプログラムを作成(Node.js)

今回は、指定された年月のカレンダーをコンソール上に表示するプログラムを作成しました。
もし年月が指定されない場合、現在の年月が使用されます。
この処理の中心として、Luxonという日付・時刻操作のためのライブラリと、コマンドライン引数を簡単に扱えるminimistが利用されています。

1. 必要なモジュールのインストール
$ npm install luxon minimist
2. 引数の受け取り
  • ユーザーが指定する年や月を簡単に取得するためにminimistを利用。指定がない場合は、現在の年月をデフォルトとして取得。
3.カレンダーの出力
  • 指定された年月の初日と最終日をluxonで取得し、日付を順に出力。
    ここで、月の初日が火曜日や水曜日など、日曜日以外の場合に、適切なスペースを空けるためにString.prototype.repeat()メソッドを使用。(指定された回数だけ文字列を繰り返すことができる。)
4. 使い方
  • プログラムを実行する際、年や月を指定してカレンダーを表示できる。
$ node calendar.js -y 2023 -m 8

指定しない場合、現在の年月のカレンダーが表示される。

$ node calendar.js

ESLintとPrettierの設定のところ

毎回ESLintPrettierのインストールや設定方法が曖昧だったので、こちらにまとめておきます。

ESLint(静的検証ツール)

JavaScriptのコード解析ツール。コードスタイルの指針に従った書き方をしているかチェックし、品質を保つのに役立つ。 Documentation

1. インストール

プロジェクトのルートディレクトリで以下のコマンドを実行。

$ npm install eslint --save-dev
2. 設定ファイルの作成

Configure ESLint

設定ファイルである.eslintrc.*.js, .json, .yml)を作成。
package.jsonがすでに存在することが前提。
eslint --initでの対話形式での作成も可能。
参考:ESLintのセットアップ方法

3. 実行
// 指定されたファイル名に対してESLintを実行して、コードの潜在的な問題点やスタイルガイド違反を警告やエラーとして出力。自動修正は行わない。
$ npx eslint <ファイル名>

// `--fix`オプションを付けることで、ESLintが自動で修正を行い、コードを修正。
$ npx eslint <ファイル名> --fix

Prettier(コードフォーマッター)

コードを美しく整形するツール。自動整形が可能で、コードの品質を維持しつつ、統一されたフォーマットを保てる。
Documentation

1. インストール

プロジェクトのルートディレクトリで以下のコマンドを実行。

$ npm install prettier --save-dev
2. 実行
// カレントディレクトリ内のファイルに対して、Prettierを実行し、ファイルを書き換え、自動フォーマットを行う。
$ `npx prettier --write .` 

// カレントディレクトリ内のファイルに対して、Prettierを実行し、ファイルがPrettierフォーマットに従っているかどうかを確認。ファイルがPrettierフォーマットに従っていない場合、エラーを表示。
$ `npx prettier --check .`
3. eslint-config-prettierのインストール

Prettierと競合するESLintのルールを無効にするための設定。

Integrating with Linters

eslint-config-prettier

npm install eslint-config-prettier --save-dev

所感✏️

ソースコードの品質を保つのに、これらのツールは必要不可欠だと感じました。
ミスを減少させ、より重要な部分に集中できるのは大きなメリットです。
TypeScriptやReact用の設定もあるので、プロジェクトを通してさらに学びたいと思います。

バージョン管理ツールnvmについてのガイド(Node.js)

この記事では、Node.jsのバージョン管理ツール、nvmのインストールと使い方を説明します。nvmは異なるプロジェクトで異なるNode.jsのバージョンを使用する必要がある場合に、それぞれのバージョンを簡単に切り替えることが可能です。

1. nvmのインストール

最初にnvmをインストール。下記のコマンドを実行。

$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash

※上記のコマンドでv0.39.3という最新バージョンをインストールしていますが、実行前に最新バージョンを確認します。

インストール後、 ~/.zshrcが以下のように更新されたことを確認。(Bash Completion)

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # This loads nvm bash_completion

ターミナルを再起動後、以下のコマンドを実行してnvmが正しくインストールされたことを確認。

$ nvm -v
2. .nvmrcファイルの自動読み込み設定

.nvmrcファイルを使うと、プロジェクトごとに特定のNode.jsのバージョンを指定できる。.nvmrcの中身はバージョン番号のみで、下記のように設定できる。

$ echo "5.9" > .nvmrc
$ echo "lts/*" > .nvmrc # to default to the latest LTS version
$ echo "node" > .nvmrc # to default to the latest version

また、~/.zshrcに以下の設定を追加することで、.nvmrcファイルが存在するディレクトリに移動すると自動的にそのバージョンを使用するように設定できる。

# place this after nvm initialization!
autoload -U add-zsh-hook
load-nvmrc() {
  #... omitted for brevity ...
}
add-zsh-hook chpwd load-nvmrc
load-nvmrc
3. nvmの使用方法

ここでは、nvmを使ってNode.jsのLTSバージョンをインストールし、デフォルトのバージョンとして設定する方法を示す。詳細な使用方法はUsageを参照。

まず、以下のコマンドでNode.jsのLTSバージョンをインストール。

$ nvm install --lts

次に、以下のコマンドでデフォルトのNode.jsのバージョンを設定。

$ nvm alias default lts/*

最後に、以下のコマンドで現在使用しているバージョンを確認。

$ nvm current

nvmの基本的な使い方について学ぶことができました。今後もNode.jsを効率的に使用するために、nvmを活用していきたいです。

ESMを使う(Node.js)

今回はNode.jsでESM (ECMAScript Modules)を使う方法をまとめます。

  • Node.js v14以降であれば、ESモジュールを使うためのサポートが組み込まれている。
その1. package.json"type": "module"を指定

package.json

{
  "name": "my-esm-project",
  "version": "1.0.0",
  "type": "module"
}

utils.js

export function greet(name) {
  return `Hello, ${name}!`;
}

main.js

import { greet } from './utils.js';

console.log(greet('World'));
$ node main.js
Hello, World!
  • プロジェクトのメタデータと依存関係をpackage.jsonに記述。開発に必要なモジュールやスクリプトもここに記述。
  • "type": "module":Node.jsがESM (ECMAScript Modules) 形式をデフォルトで使用するように指定する。
    • Node.jsは長らくCommonJS形式をモジュールシステムとして利用していたが、最近ではJavaScriptがネイティブにサポートするESM形式を利用することが増えている。
    • 後方互換性を維持するために、既存のコードが依然としてCommonJS形式を使用できるようにする。"type": "module"package.jsonに記述することで、モジュールをESM形式として解釈。これにより、.jsファイルでimportexport構文を使用できる。
その2. モジュールファイル.mjsの拡張子にする(業務でも使うらしい)

main.mjs

import fs from 'fs';

fs.readFile('./package.json', 'utf-8', (err, data) => {
  if (err) throw err;
  console.log(data);
});
  • Node.jsの組み込みモジュールであるfsをインポートし、package.jsonファイルを読み込んでその内容をコンソールに出力。以下で実行。
$ node main.mjs
  • "type": "module"package.jsonに指定されていない場合や、一部のファイルだけをESM形式として扱いたい場合などに、ファイルの拡張子を.mjsにすることでそのファイルをESM形式として扱うことができる。これにより、そのファイルではimportexport構文が使用できる。

【参考】

lsコマンドを作る オブジェクト指向版

以前、Rubyの標準ライブラリのみで実装したLinuxUnix系のOSで使われるlsコマンドを、オブジェクト指向版として実装しました。

要件
  • オプションなし 最大3列で表示。 ファイルの並び順は以下のとおり。
1 5 9
2 6 10
3 7
4 8
  • -a-r-lオプションを併用できる形で実装する。

  • 実装の対象外の機能

    • 引数にファイルやディレクトリを指定可能にする。
    • mac拡張属性(@マーク)の表示。

ls_command.rb

  • コマンドラインからの入力を受け取り、それに基づいて上記のクラスを利用してファイル一覧を表示。OptionParserを用いてコマンドライン引数(-a, -r, -l)を解析し、その結果をFileListクラスのインスタンスに渡して表示を行う。
#!/usr/bin/env ruby
# frozen_string_literal: true

require 'optparse'
require_relative 'file_info'
require_relative 'file_permission'
require_relative 'file_list'

params = {}
OptionParser.new do |opt|
  opt.on('-a') { |v| params[:a] = v }
  opt.on('-r') { |v| params[:r] = v }
  opt.on('-l') { |v| params[:l] = v }
  opt.parse!(ARGV)
end

file_list = FileList.new(params)
file_list.display_lists

file_info.rb

  • 特定のファイルに関する情報を取得。
# frozen_string_literal: true

require 'etc'

class FileInfo
  attr_reader :file

  def initialize(file)
    @file = file
  end

# ファイルの統計情報(サイズ、所有者、グループ、更新時間など)を取得
  def stat
    File.stat(file)
  end

# ファイルのパーミッションをFilePermissionクラスを用いて取得
  def permissions
    FilePermission.new(file).format_permissions
  end

# ファイルのハードリンク数を取得
  def hard_link
    stat.nlink
  end

# ファイルの所有者のユーザー名を取得
  def user
    Etc.getpwuid(stat.uid).name
  end

# ファイルの所有グループ名を取得
  def group
    Etc.getgrgid(stat.gid).name
  end

# ファイルのサイズを取得
  def filesize
    stat.size
  end

# ファイルの最終更新時間を取得
  def mtime
    File.mtime(file)
  end
end

file_list.rb

  • ディレクトリ内のファイル一覧を取得し、その一覧を指定された形式で表示。
# frozen_string_literal: true

class FileList
  COLUMNS = 3

  def initialize(params)
    @params = params
  end

  def display_lists
    file_match_option = @params[:a] ? File::FNM_DOTMATCH : 0
    files = Dir.glob('*', file_match_option).map { |file| FileInfo.new(file) }
    current_files = @params[:r] ? files.reverse : files
    @params[:l] ? display_in_long_format(current_files) : display_in_short_format(current_files)
  end

  private

  def display_in_short_format(lists)
    max_filename_length = lists.map { |file_info| file_info.file.length }.max
    line_count = (lists.length.to_f / COLUMNS).ceil
    line_count.times do |line|
      lists.each_slice(line_count) do |columns|
        print columns[line].file.ljust(max_filename_length + 2) if columns[line]
      end
      print "\n"
    end
  end

  def display_in_long_format(current_files)
    display_total_block_number(current_files)
    max_lengths = calculate_maximum_lengths(current_files)
    current_files.each { |file_info| display_file_details(file_info, max_lengths) }
  end

  def display_total_block_number(current_files)
    total = current_files.sum { |file| file.stat.blocks }
    puts "total #{total}"
  end

  def display_file_details(file_info, max_lengths)
    permissions = file_info.permissions
    hard_link = file_info.hard_link.to_s.rjust(max_lengths[:hard_link])
    user = file_info.user.rjust(max_lengths[:user])
    group = file_info.group.rjust(max_lengths[:group])
    filesize = file_info.filesize.to_s.rjust(max_lengths[:filesize])
    time = file_info.mtime.strftime('%m %d %H:%M')
    filename = file_info.file
    puts "#{permissions}  #{hard_link} #{user}  #{group}  #{filesize} #{time} #{filename}"
  end

  def calculate_maximum_lengths(file_stats)
    max_lengths = {
      hard_link: 0,
      user: 0,
      group: 0,
      filesize: 0
    }

    file_stats.each do |file|
      max_lengths[:hard_link] = [max_lengths[:hard_link], file.hard_link.to_s.length].max
      max_lengths[:user] = [max_lengths[:user], file.user.length].max
      max_lengths[:group] = [max_lengths[:group], file.group.length].max
      max_lengths[:filesize] = [max_lengths[:filesize], file.filesize.to_s.length].max
    end

    max_lengths
  end
end

file_permission.rb

  • ファイルのパーミッションを表す文字列を生成。 File.lstat(file).mode.to_s(8)を使用してファイルのモード(パーミッションとファイルの種類を含む)を8進数の文字列として取得し、読みやすい形式(例: 'rwxr-xr-x')に変換。
class FilePermission
  PERMISSIONS = ['---', '--x', '-w-', '-wx', 'r--', 'r-x', 'rw-', 'rwx'].freeze

  FILE_TYPE = {
    '10' => 'p',
    '20' => 'c',
    '40' => 'd',
    '60' => 'b',
    '100' => '-',
    '120' => 'l',
    '140' => 's'
  }.freeze

  attr_reader :file

  def initialize(file)
    @file = file
  end

  def format_permissions
    type_of_file + permission_code
  end

  private

  def type_of_file
    FILE_TYPE[file_mode[0..-4]]
  end

  def permission_code
    permission_numbers.map { |n| PERMISSIONS[n] }.join
  end

  def file_mode
    File.lstat(file).mode.to_s(8)
  end

  def permission_numbers
    file_mode[-3..].chars.map(&:to_i)
  end
end

【参考】

ボウリングのスコア計算プログラム オブジェクト指向版

以前、Rubyの標準ライブラリのみで実装したボウリングプログラムを、オブジェクト指向版として実装しました。

1. 要件(ルール)
  • 1ゲーム=10フレーム
  • 1フレーム=2投(例外あり)
  • ピンの数は10本
  • 1投目で10本倒したらストライク
  • ストライクの場合は2投目は表記しない
  • 1投目で全て倒せなかった時、2投目で全て倒したらスペア
  • スペアのフレームの得点は次の1投の点を加算する
    • 例: 6 4 5 = 15
    • 例: 6 4 10 = 20
  • ストライクのフレームの得点は次の2投の点を加算する
    • 例: 10 5 2 = 17
    • 例: 10 10 10 = 30
  • 10フレーム目は1投目がストライクもしくは2投目がスペアだった場合、3投目が投げられる
  • ありえない投球数やありえない数字・記号がこない前提

bowling.rb(実行ファイル)

#!/usr/bin/env ruby
# frozen_string_literal: true

require_relative 'game'

shots = ARGV[0].split(',')

game = Game.new(shots)
puts game.score
  1. ARGV[0]コマンドライン引数を取得し、カンマで区切られたショットのデータを配列に分割。
  2. Gameクラスのインスタンスを生成し、ショットのデータを渡してゲームを開始。
  3. 最後に、ゲームのスコアを表示。

game.rb

# frozen_string_literal: true

require_relative 'frame'

class Game
  def initialize(shots)
    @shots = shots
  end

  def score
    @frames = create_frames
    total_score = 0
    (0..9).each do |frame_index|
      frame = Frame.new(@frames[frame_index])
      total_score += frame.score
      @frames[frame_index + 1] ||= []
      @frames[frame_index + 2] ||= []
      total_score += calculate_bonus_points(frame_index, frame)
    end

    total_score
  end

  def create_frames
    frame = []
    frames = []
    @shots.each do |s|
      frame << s
      if frames.length < 10
        if frame.length == 2 || s == 'X'
          frames << frame.dup
          frame.clear
        end
      else
        frames.last << s
      end
    end
    frames
  end

  def calculate_bonus_points(frame_index, frame)
    if frame.strike?
      next_two_shots = (@frames[frame_index + 1] + @frames[frame_index + 2]).slice(0, 2)
      next_two_shots.sum { |s| Shot.new(s).score }
    elsif frame.spare?
      next_shot = @frames[frame_index + 1][0]
      Shot.new(next_shot).score
    else
      0
    end
  end
end
  1. initializeメソッドでは、ショットのデータを受け取り、インスタンス変数@shotsに格納。
  2. scoreメソッドはゲームのスコアを計算して返す。
    • まず、create_framesメソッドを呼び出してフレームの配列@framesを作成。
    • 次に、total_score変数を0で初期化。10フレーム分の処理を繰り返し、各フレームのスコアを計算し、total_scoreに加算。
    • また、ボーナスポイントも計算して加算。
    • 最後に、計算された総合スコアを返す。
  3. create_framesメソッドは、ショットのデータをフレームごとに分割し、フレームの配列framesを作成。
    • frame配列にショットを追加していき、フレーム数が10未満の場合はフレームが完了した時点で新しいフレームを作成し、フレーム数が10の場合は最後のフレームに追加のショットを結合。
  4. calculate_bonus_pointsメソッドは、フレームのインデックスとフレームオブジェクトを受け取り、ボーナスポイントを計算して返す。 また、与えられたフレームがストライクの場合とスペアの場合で異なるボーナスポイントの計算を行う。
    • まず、フレームがストライクの場合は、次の2つのフレームのショットを取得し、それらのスコアを計算してボーナスポイントとする。@frames[frame_index + 1]@frames[frame_index + 2]を連結して、最初の2つのショットを取得。
    • そして、それぞれのショットをShotクラスのインスタンスとして生成し、スコアを取得。取得したスコアを合計した値がボーナスポイントとなる。
  5. フレームがスペアの場合は、次のフレームの最初のショットのスコアをボーナスポイントとする。@frames[frame_index + 1]の最初の要素を取得し、それをShotクラスのインスタンスとして生成し、スコアを取得。
  6. 最後に、フレームがストライクでもスペアでもない場合は、ボーナスポイントを0とする。 これにより、ゲームのスコア計算においてストライクやスペアの場合のボーナスポイントが正しく計算され、総合スコアに加算される。

(0..9).each do |frame_index|...endの箇所については、injectを使っても良い。

frame.rb

# frozen_string_literal: true

require_relative 'shot'

class Frame
  def initialize(frame)
    @first_shot = Shot.new(frame[0])
    @second_shot = Shot.new(frame[1])
    @third_shot = Shot.new(frame[2])
  end

  def score
    [
      @first_shot.score,
      @second_shot.score,
      @third_shot.score
    ].sum
  end

  def strike?
    @first_shot.score == 10
  end

  def spare?
    score == 10
  end
end
  1. initializeメソッドでは、frameという引数を受け取り、その値を元にフレーム内の各ショットを表すインスタンス変数@first_shot@second_shot@third_shotを作成。
  2. scoreメソッドは、各ショットのスコアを取得し、それらの合計値を返す。
  3. strike?メソッドは、フレームの最初のショットがストライクであるかどうかを確認。
  4. spare?メソッドは、フレームのスコアがスペアであるかどうかを確認。

shot.rb

# frozen_string_literal: true

class Shot
  def initialize(mark)
    @mark = mark
  end

  def score
    @mark == 'X' ? 10 : @mark.to_i
  end
end
  1. initializeメソッドでは、markという引数を受け取り、その値をインスタンス変数@markに格納。
  2. scoreメソッドは、ショットのスコアを計算して返す。もし@markが'X'(ストライク)であれば10を返し、そうでなければ@markを整数に変換して返す。