npm icon-gen v1.2.0 release

2017年11月21日 0 開発 , ,

icon-gen v1.2.0 をリリースした。

今回の目玉は ICNS における is32il32 のサポート。Wikipedia の Apple Icon Image format によると ICNS は現行の macOS なら ic07ic14 があれば十分にみえる。

しかし GitHub にて Mac OS X finder uses also is32 and il32 icns. という Pull Request があった。どうやら is32il32 も必要とのこと。これらがないと Finder のリスト表示でアイコンが消えるらしい。

この報告をうけて High Sierra 環境でリスト表示を試したものの、正常に表示されていたので古い macOS 固有の問題かもしれない。私の環境だと再現できないので Pull Request をそのまま採用しようかな?と実装を確認したところ is32il32 の画像部分が PNG ベタ書きになっていた。

本来、これらの画像部分は特殊な圧縮がかかっている。以前、サポートしようとしときに見つけた以下のページによれば PackBits 形式らしい。また RGBA のうち R、G、B をチャンネル単位で圧縮し、A は s8mkl8mk という対となるマスクとして別ブロックに書き込むのだという。

このときは面倒そうだし、私の環境では問題おきてないし、なにより将来なくなるであろうレガシーな形式であることからサポートを見送った。しかし Pull Request がきたのを契機にあらためて PackBits 圧縮を検討してみることにした。

PackBits

PackBits は Run Length Encoding (以下、RLE) の一種である。

アルゴリズムとしては非常に単純なため様々な言語で実装されている。npm だと packbits が見つかった。しかしこれは String が対象。icon-gen 的には Buffer と Array、つまりバイナリーとして扱いたいので別の実装をあたることにした。結果、

が癖もなく分かりやすかったので Node に移植してみた。

PackBits は Apple がまだ Apple Computer だった時代に圧縮と展開のサンプル データを公開しており Wikipedia にもそれが掲載されている。元コードにはテストがなかったので、このサンプルを基準としたテストを実装し、正常に動作することが確認できた。

ではさっそく ICNS へ!というわけで動かしてみたところ、出力されたアイコンを macOS の Preview で確認すると色がめちゃくちゃだ。Alpha にあたる s8mkl8mk は無圧縮なので、これにあたる透過だけが正常に描画される状態。

PackBits としてはサンプルの圧縮と展開に成功、つまり正常と思われるのだが。…もしかして PackBits 圧縮ではない?

ICNS 専用 RLE

改めて Wikipedia の ICNS ページを読みなおしたら

Over time the format has been improved and there is support for compression of some parts of the pixel data. The 32-bit (“is32”, “il32”, “ih32″,”it32”) pixel data are often compressed (per channel) with a format similar to PackBits.[1]

PackBits ではなく似て非なるものらしい。出典としてあげられてる資料も確認。

あらら、やはり PackBits ではないのか。RLE の一種であることは確かだが Apple による公式な仕様はなくて Peter Stuer 氏がリバース エンジニアリングしたのだという。G. Brannon Smith による Java 実装もあるらしい。

とりあえず今後の開発にそなえて is32il32 の実データがほしい。しかし High Sierra に付属している iconutil でアイコン生成するとこれらは存在せず、かわりに ic04ic06 が埋め込まれている。

じゃあこれらを書けばいいのでは?とバイナリー エディタで調べてみたら画像部分の先頭に ARGB とあり、PNG とも RLE とも異なるようだ。これについては別途調査するとして、既存アプリのアイコンならどうかと Firefox.app から持ってきたものを調べたら is32il32 が存在した。

テスト用に ICNS からブロックのヘッダーとボディを抽出するメソッドを実装し、

export default class ICNSGenerator {
  /**
   * Unpack an icon block files from ICNS file (For debug).
   *
   * @param {String} src  Path of the ICNS file.
   * @param {String} dest Path of directory to output icon block files.
   *
   * @return {Promise} Promise object.
   */
  static _debugUnpackIconBlocks (src, dest) {
    return new Promise((resolve, reject) => {
      Fs.readFile(src, (err, data) => {
        if (err) {
          return reject(err)
        }

        for (let pos = HEADER_SIZE, max = data.length; pos < max;) {
          const header = data.slice(pos, pos + HEADER_SIZE)
          const id     = header.toString('ascii', 0, 4)
          const size   = header.readUInt32BE(4) - HEADER_SIZE

          pos += HEADER_SIZE
          const body  = data.slice(pos, pos + size)
          Fs.writeFileSync(Path.join(dest, id + '.header'), header, 'binary')
          Fs.writeFileSync(Path.join(dest, id + '.body'), body, 'binary')

          pos += size
        }

        resolve()
      })
    })
  }
}

Firefox から当該ブロックを取得。また il32 に相当する 32×32 の PNG を得るため ic11 のボディを PNG として保存。icon-gen は SVG だけでなく PNG からも ICNS を生成できるので、これを使用して Firedox の il32 と一致するバイナリーを生成できれば OK というわけだ。

参考資料にある libicns の実装を眺めてみる。

ソース コードとしては icns_rle24.c
icns_encode_rle24_data が ICNS の RLE 処理。ありがたいことに ImageMagick といった外部ライブラリーには依存せず、自己完結している。バイト配列の操作と標準関数のみで実装されているため Node にも移植しやすそうだ。

というわけで移植版を動作させたところ、出力されたバイナリーは見事にサンプルと一致した。ICNS ファイルを Preview で開いても正常に描画されている。

しかし libicns のライセンスは GPL である。icon-gen は既に MIT ライセンスで公開しており Dependents も 4。それらは MIT か Apache-2.0 なので icon-gen が libicns 移植を採用すると Copyleft により Dependents にも影響がある。

ならばスクラッチか。既に移植しているため完全なクリーン ルーム開発はできないが
icns_encode_rle24_data にまとまった仕様がコメントされている。

// Assumptions of what icns rle data is all about:
// A) Each channel is encoded indepenent of the next.
// B) An encoded channel looks like this:
//    0xRL 0xCV 0xCV 0xRL 0xCV - RL is run-length and CV is color value.
// C) There are two types of runs
//    1) Run of same value - high bit of RL is set
//    2) Run of differing values - high bit of RL is NOT set
// D) 0xRL also has two ranges
//    1) for set high bit RL, 3 to 130
//    2) for clr high bit RL, 1 to 128
// E) 0xRL byte is therefore set as follows:
//    1) for same values, RL = RL - 1
//    2) different values, RL = RL + 125
//    3) both methods will automatically set the high bit appropriately
// F) 0xCV byte are set accordingly
//    1) for differing values, run of all differing values
//    2) for same values, only one byte of that values
// Estimations put the absolute worst case scenario as the
// final compressed data being slightly LARGER. So we need to be
// careful about allocating memory. (Did I miss something?)
// tests seem to indicate it will never be larger than the original

雑に翻訳。原文と訳は libicns にならって GPL とする。

ICNS RLE データの仮定 :
A) 各チャンネルは次のチャンネルから独立してエンコードされる
B) エンコードされたチャンネルは以下のようになります
   0xRL 0xCV 0xCV 0xRL 0xCV - RL は Run Length で CV はカラー値です
C) 2 種類の実行 (Run)
   1) 同じ値の実行 - RL の上位ビットを設定します
   2) 異なる値の実行 - RL の上位ビットは設定されません
D) 0xRL には 2 つの範囲もあります
   1) 上位ビットの設定された RL は 3 〜 130
   2) 上位ビットの設定されない RL は 1 〜 128
      ※原文の "clr" は文脈的に clear の略 = 「設定されない」と解釈
E) 0xRL バイトは以下のように設定されます :
   1) 同じ値は RL = RL - 1
   2) 異なる値は RL = RL + 125
   3) 両方のメソッドは自動かつ適切に上位ビットを設定します
F) 0xCV はそれ (0xRL) に応じて設定されます
   1) 異なる値の場合は、すべての異なる値の実行
   2) 同じ値なら、その値を 1 バイトだけ

最終的な圧縮データはわずかに大きいため、最悪な場合のシナリオで見積もります。そのため私たちはメモリ割り当てについて注意する必要があります。 (なにか私は見落としていますか?)
テストではそれが元よりも大きくなることはないと考えられます。

これを見ながら途中まで実装したのだが、そういえばこの特殊な RLE がリバース エンジニアリングによって解明されたこと、Java 実装があるらしいことを思い出す。ならばそれもチェックしておこうかと探してみたら以下をみつけた。

ImageJ という Java の画像処理ライブラリーの一部らしい。ライセンスはコード冒頭をみるに修正 BDS。もしこれを移植して動くようなら採用しよう。

  • libicns 移植版で生成されたバイト配列をテストの基準とする
  • ImageJ 移植版で生成されたバイト配列がテストを満たすようにする

という条件のもとに移植。結果、テストをパスして icon-gen が出力したアイコンも正常に書き込まれていた。icon-gen v1.2.0 としてはこの実装を採用。

オマケとして libicns 移植版の圧縮処理を Gist へ公開。ライセンスはオリジナルにもとづき GPL。

iconutil の出力形式

macOS 付属の CLI ツール、iconutil により ICNS ファイルを生成できる。使用方法と必要なファイルについては以下の記事がわかりやすい。

さて、前述のように High Sierra と未満で iconutil が出力したファイル内の構成が異なる。icon-gen としては High Sierra 未満の ICNS を出力しているわけだが、現行のものへ正式対応する際の参考に調査した形式をまとめておく。

旧は Sierra、新は High Sierra の iconutil で出力したもの。一方にしかないものは他方を空欄してある。画像サイズは正方形だが、そうであることを明示するため WxH 形式で記述しておく。

サイズ 内容
is32 16×16 R、G、B を独立して特殊 RLE 圧縮。RRR、GGG、BBB と並ぶ。
s8mk 16×16 is32 の透過部分。RGBA の A を無圧縮。
il32 32×32 R、G、B を独立して特殊 RLE 圧縮。RRR、GGG、BBB と並ぶ。
l8mk 32×32 il32 の透過部分。RGBA の A を無圧縮。
ic04 16×16 32bit ARGB。画像形式は謎。
ic05 32×32
ic06 48×48
ic07 ic07 128×128 32bit ARGB。画像形式は PNG ファイル。
ic08 ic08 256×256
ic09 ic09 512×512
ic10 ic10 1024×1024
ic11 ic11 32×32
ic12 ic12 64×64
ic13 ic13 256×256
ic14 ic14 512×512
info info バイナリー形式の plist。plutil で XML 化すると iconutil のバージョン情報などが定義されていることを確認できる。

icon-gen は旧形に準拠するが info は書き出していない。plist バイナリーを埋め込んでもよいけれど、これなしにも有効な ICNS と見なされるようなので無視している。

Issue #54 で調査した際は Safari v10.1 (12603.1.30.0.34) が旧式で Firefox 54 は旧式から ic07ic11ic14 が抜けた状態だった。

まとめ

長らく懸念だった is32il32 処理が解決されてうれしい。

ところで今回は C# (PackBits)、C 言語と Java (特殊 RLE) を移植してみたのだが、Node の標準 API と JavaScript の Array が高機能なおかげで移植しやすかった。開発プラットフォームとして必要十分な機能はあるので OS やハードウェア依存の API (Win32 とか DirectX など) を使用していなければ、Node は移植先として有望と感じた。

しかし今回は局所的な移植だったからよかったものの、大規模なものはどうなのだろう。ある時点の移植はできても本家の変更に追従し続けるのは極めて難しそう。sqlite3 は移植ではなく node-gyp による wrapper となっているのは、この事情を踏まえてのことだろう。

などと考えていたらタイムリーな記事がはてブのホット エントリーにあがっていた。

C 言語の実装を kripken/emscripten で WASM にビルドして npm 配布したのだという。これは面白い。

Node は v8.0.0 から、Chrome も v58 から WASM に対応している。Electron でいうと v1.7.0 で Chrome v58、v1.8.0 から Node v8.2.1 を採用しているため、Electron v1.8 系なら Main/Renderer プロセスのどちらでも WASM を使用できるはず。

つまり豊富な C 言語の資産を流用しやすくなるのだ。すばらしい。

WASM は機能の縛りが厳しいため移植を完全に自動化するのは難しいだろうけど、そのへんは emscripten のような周辺ツールが解決すると予想している。例えば File I/O などが含まれていたら WASM とその wrapper で分担・抽象化するとか。

例えば前述の SQLite が WASM 化されたなら Electron アプリに組み込みやすくなるだろう。WASM はパフォーマンスよりもこのような移植方面で期待している。

npm 開発で脱 Babel してみる

2017年1月25日 0 開発 , , ,

自作 npm の開発で脱 Babel したときの対応と問題点まとめ。

  • 2017/1/31 訂正
    power-assert の作者、t_wada さんより power-assert は babel-register を通すなどしないと assert 置換が働かず素の assert になってしまうという指摘があったのでユニットテスト関連の記述を訂正

脱 Babel を決めた背景

私はいくつか自作 npm を公開している。これらは ES2015 以降の機能と構文を利用して npm publish の際に Babel で ES5 相当へ transpile している。この運用で特に問題も起きていない。またプリセットに babel-preset-latest を採用することで ES 関係の規格追従を Babel 任せにできる安心感もあり、ずっとこのままでいいと思っていた。

ある日、職場で Node アプリを開発している人から「Babel 依存は怖くないですか?」という質問があった。Babel にバグがあったら調査や修正は困難だし、使わなくてよいならばそうするに越したことはないのでは?と。

これまで C++、C#、Java などコンパイル前提の言語で開発した経験から、よほどのことがない限りコンパイル結果は信頼に足ると判断していた。また、コンパイルされたマシン語や中間言語を人間が直に記述するのは非常にキツイ。そのため脱コンパイラーという選択肢は現実的ではないと認識している。

一方、JavaScript における transpile は高級言語どうしの変換となる。その気になれば人間が書けるのだ。

transpiler への慣れから、この事実をすっかり忘れていた。Node であれば Web フロントエンドと異なり動作環境の分岐は少なく、package.json の engines にて対象環境を限定することも可能である。ならば新しい Node を前提として脱 Babel を検討してみるのもよさそうと考えはじめた。

そんな折、npm-run-all が v4.0.0 で Babel による transpile を廃止。Node は v4 時点で ES Modules を除く大半の ES2015 機能が実装されているため、この範囲で足りるなら transpile せずにそのままリリース可能だ。その前例として普段利用しているツールが脱 Babel したのはインパクトある。

これらを踏まえ、まずは自作 npm のうちダウンロード数の少なくニッチな wpxml2md から脱 Babel を試してみることにした。

脱 Babel への道のり

脱 Babel 対応で実施したことを書く。

Node 環境の明示的な指定

脱 Babel における前提条件として動作環境とする Node のバージョンを決める。npm-run-all は Node v4 を下限としているようだが、wpxml2md では v6 としておく。

開発と検証コストを考慮して自作 npm の動作環境は「最新 + 最新 LTS」としている。2017/1 時点の最新 Node は v7 系、LTS は v6 と v4 があるため対象は「v7 + v6」となる。これまでは Babel による transpile で v4 以下でも動作していたのだが、これを廃止することで明示的な下限の指定が必要となった。これは package.json の engines プロパティに記述する。

{
  "engines": {
    "node": ">= 6"
  }
}

ちなみに

node v6, v7

というバッヂを用意していて前から README へ掲載していた。今回の対応により、ようやくこれが本来の意味をあらわすようになった。

Babel の transpile を廃止

これまでは Babel の transpile を前提として以下のような Babel 設定と npm-scripts を利用していた。

{
  "babel": {
    "presets": [
      "latest"
    ],
    "env": {
      "development": {
        "presets": [
          "power-assert"
        ]
      }
    }
  },
  "scripts": {
    "test": "mocha --timeout 50000 --compilers js:babel-register test/**/*.test.js",
    "watch": "babel src --out-dir ./ --watch",
    "start": "npm run watch",
    "build": "babel src --out-dir ./",
    "prepublish": "npm run build"
  }
}
script 内容
test 予約された npm run のタスク。 mocha と power-assert によるユニット テスト。
watch 開発用。ファイル監視による自動 transpile を実行。
start 予約された npm run のタスク。watch を呼び出すだけ。
build リリース用。現時点のソース コードで transpile を実行。
prepublish 予約された npm publish 時に呼び出されるタスク。build を呼び出しているため npm として公開されるイメージは transpile されたものになる。なお prepublish は現在 deprecated になっていて prepare へ修正すべきなのだが直し忘れていた。

これが脱 Babel によりこうなる。

{
  "babel": {
    "env": {
      "development": {
        "presets": [
          "power-assert"
        ]
      }
    }
  },
  "scripts": {
    "test": "mocha --timeout 50000 --compilers js:babel-register test/**/*.test.js"
  }
}

transpile 不要となるため関連するタスクが消え、ユニット テストだけが残った。ただし power-assert を利用しているなら標準 assert を置換するための transpile が必要なので、このための設定は維持する。

env.development.presetspower-assert だけ指定することで、Babel 依存がユニット テストに限定されていることが明示されるだろう。必要な Babel 関連の npm も babel-registerbabel-preset-power-assert だけになる。

ES Moduels を CommonJS 化する

Node の ES Modules 対応については以下が詳しい。

なお現時点の最新 Node である v7 においても ES Modules には対応していないため、export/import は CommonJS の exports/require へ修正する必要がある。例えば

export const Options = {
};

export default class CLI {
}

const Options = {
};

class CLI {
}

module.exports = {
  Options: Options,
  CLI: CLI
};

とする。読み込む側は

import CLI from `cli.js`;
import { Options } from `cli.js`;

const CLI = require( `cli.js` ).CLI;
const Options = require( `cli.js` ).Options;

とする。ひとつのモジュールから exportdefault export で複数のインターフェースを公開していると面倒である。しかし ES Modules で書いていたなら import はソース コード冒頭に集約されているから悩む余地なく機会的な作業になる。なんなら正規表現でまとめて変換できる。

ユニット テストについても同様に対応すること。

余談。

CommonJS にした後でも将来の ES Modules 移行を容易にするため require はソース コード冒頭へ書く習慣をつけたほうがよいかもしれない。その場合、読み込み先を camelCase で命名しているとローカル変数と競合する可能性が高くなるため PascalCase にしたくなり、私はそうしている。例えば fsFs と命名している。

Node と CommonJS だとスコープを意識して require を使い分け、なるべく関数ローカルで宣言する派が多数な感じなので ES Modules 対応されたときが気になる。私のようにするか、それともよりよい慣習となるのか?実に楽しみだ。

ESDoc 対応

npm のコード ドキュメント生成に ESDoc を採用している場合、そのままでは CommonJS を解釈できないので対応が必要になる。CommonJS は

上記 issue で紹介されている esdoc-node により対応できる。ESDoc にプラグイン機能があることを初めて知った。これを指定することでコードが ESDoc に解釈される前処理を実行できるらしい。例えば esdoc-node は CommonJS を ES Modules に変換して ESDoc に渡す。

さっそく使ってみよう。まず esdoc-node をプロジェクトに追加。

$ npm install -D esdoc-node

次にこれを ESDoc 設定へ追加する。私は ESDoc の設定を package.json に定義しているので

{
  "esdoc": {
    "source": "./src",
    "destination": "./esdoc",
    "test": {
      "type": "mocha",
      "source": "./test"
    },
    "plugins": [
      { "name": "esdoc-node" }
    ]
  }
}

このようにした。設定後、ESDoc を実行して CommonJS なコードも正しく解析されることを確認。

ただしローカル実行ではなく ESDoc Hosting Service は CommonJS やプラグインに対応していないようだ。試しに wpxml2md の API Document を生成 してみたのだが、2017/1/25 時点では esdoc-node 未指定の状態と同様に CommonJS の絡むものが解釈できていない。

この点について要望 issue を登録してみたのだけど、もしプラグイン対応する場合

  • Hosting Service 側でプラグインを網羅しておき選択実行する
    • プラグインを中央管理する仕組みがないと網羅できない
    • プラグインのバージョンはどうするのか?常に最新?
  • プロジェクトの ESDoc 設定と package.json から動的にプラグインを決定する
    • プラグインのインストールはどうする?
    • ドキュメント生成とごにプラグインをインストールすると Hosting Service の負荷が大きい
    • CI 系サービスのように VM や Docker コンテナで対応するとしても負荷は大きい

といった問題が予想されるため難しそうだ。しかし要望があることだけは記録しておきたかったので issue 登録することにした。

別の方法として ESDoc 本体が esdoc-node 処理を取り込むという選択肢もある。ただ、いずれ Node が ES Modules へ正式に対応するとしたら CommonJS は過渡期の存在である。

  • そのために対応コストを割くのか?
  • ESDoc と名乗るツールとして ECMAScript とは直に関係しない CommonJS へ対応することは設計思想として望ましくないのでは?

という考えもあるだろう。どのような対応、または非対応のままになったとしても ESDoc 作者の意向を尊重したい。

ESDoc Hosting Service を利用しない場合、対象プロジェクトが GitHub で管理されているならそのリポジトリに対して GitHub Pages を使う手もある。ローカルの ESDoc + esdoc-node で出力したものを自前でアップロードするか、そういうタスクを npm-scripts に定義して CI サービス経由で生成から公開まで自動化するなどの方法が考えられる。

本記事を書いた直後に ESDoc 作者の @h13i32maru さんから見解が。

ESDoc Hosting Service 的にはセキュリティや負荷を考慮し、ESDoc 公式プラグインのみサポートしているとのこと。ただ Node の ES Modules 対応は相当に先となりそうなので、この長い過渡期に Node かつ脱 Babel したい開発者としてどうか?という悩ましい課題がある。

まとめ

一部、問題もあったが transpile 不要となったことで Node のバージョンだけを意識して開発すればよい。記述したコードはそのまま実行されることが保証される。Babel 由来の潜在的なトラブルを考えなくてよいため精神的に楽。

あと npm をユニット テストではなく普通のプログラムとして走らせて検証してみたい場合、いちいち transpile しなくて済むのも助かる。

検証はテストに定義すべきでは?という意見もあるだろうけどリポジトリの examples フォルダに npm として参照したときのサンプルを配置しているとき、その動作検証で npm publish する前の現行コードを試したくなったりする。その場合、transpile なしだとビルド系タスクを実行せず動かせてよい。

以上を踏まえ、他の npm についても気が向いたら脱 Babel してゆく予定。

npm-scripts でクロスプラットフォームに環境変数を参照するための npm を作成してみた

2016年5月19日 0 開発 , , , ,

以下の記事で npm-scripts から環境変数を参照する方法と問題点について書いた。

課題として npm run するプラットフォームによって変数の参照記法が異なるため、それらを統一できないという問題がある。npm-scripts でタスク管理したい派としては、これをどうしても解決したかったので、そのための npm を作成してみた。

以下に npm の設計などをまとめる。

npm-scripts における環境変数の参照記法

冒頭のリンク先でも解説してあるが、改めて。package.json に定義された値を npm-scripts から環境変数として参照する場合の記法は以下となる。

Platform Format
OS X, Linux $npm_package_NAME or $npm_package_config_NAME
Windows %npm_package_NAME% or %npm_package_config_NAME%

今のところ標準の npm run でこれを統一する方法はないらしい。よってプラットフォーム毎に script を分けるか cross-env のような npm により scripts を wrap して参照を解決しなくてはならない。

cross-conf-env

というわけで、npm-scripts 内の環境変数に対する参照記法をクロスプラットフォームに解決する npm を開発してみた。名づけて cross-conf-env。cross-env のアイディアと設計を参考にしたので、それに準じた命名にしてある。

まずはインストール。package.json が既に定義されている状態としてコマンドを実行。

$ npm i -D cross-conf-env

cross-conf-env では以下の参照記法をサポートしている。

Platform Format
OS X, Linux $npm_package_NAME or $npm_package_config_NAME
Windows %npm_package_NAME% or %npm_package_config_NAME%
独自 npm_package_NAME or npm_package_config_NAME

これらのどれを採用してもよいし、混在も許可している。独自を選ぶと記法が統一できる。それ以外を選んだ場合はプラットフォーム、cross-conf-env の順に記法が解決される。

package.json の利用例。

{
  "name": "sample",
  "version": "1.0.0",
  "config": {
    "app": "MyApp"
  },
  "scripts": {
    "var": "cross-conf-env echo npm_package_config_app npm_package_version",
    "var:bash": "cross-conf-env echo $npm_package_config_app $npm_package_version",
    "var:win": "cross-conf-env echo %npm_package_config_app% %npm_package_version%"
  },
  "devDependencies": {
    "cross-conf-env": "^1.0.0"
  }
}

script の先頭に cross-conf-env を宣言、その後に実行したいコマンドを続ける。cross-conf-env はそれらを引数として扱い、環境変数の参照を検出したら解決してから子プロセスとしてコマンドを実行する。

設計について。

npm-cross-conf-env/cross-conf-env.js のみで完結する小さな実装。これを npm の体裁でくるんでいるだけ。おこなっていることも単純で、

  1. process.env から npm_package_ を接頭語とするプロパティを列挙
  2. process.argv の index = 1 以降を抽出、argv とする
  3. argv から 1 のプロパティ名を含む値を検索
  4. もし含むならその部分を process.env の当該プロパティの値に置換
  5. argv の先頭をコマンド、以降を引数として子プロセス起動

という感じ。

より実践的な利用方法

akabekobeko/examples-electron の各プロジェクトにある package.json を参照のこと。

これらは npm-scripts を共通にしつつ、実行ファイル名やパッケージ化に使用する Electron のバージョンを config に切り出している。そのため流用が容易になり、設定を変えたくなった場合も長大な script を慎重に直すのではなく config を書き換えるだけで済む。

環境変数の解決により npm-scripts からハードコード部分を減らせる。これを前提にするとタスクランナーとしての実用性がグッと増すはず。