20201018のNode.jsに関する記事は8件です。

M5StickCの書き換えが面倒だったので、Node.jsでArduinoっぽくしてみた

M5StickCのように、ESP32にGroveが付いているのが多いで、いろんな周辺デバイスをいじりたいのですが、そのたびに、リコンパイル&書き込みをするのはすごく面倒で時間がかかるので、MQTTを使ってリモートから操作できるようにして、クライアント側はNode.jsでたたけるようにしました。

M5StickC側では、I2C、Serial、Lcd、Gpioを操作できるようにしましたので、一度バイナリを書き込んでしまえばあとは、クライアント側のNode.jsで周辺デバイスのドライバを書くことができます。
ですが、動作はそこまで速くない(特にLcd描画系)ので、お遊び程度に思ってください。

で、Node.jsになって開発しやすくなったので、手元にあったI2Cデバイスを一気に実装しました。

・M5Stack ENVユニット
 https://www.switch-science.com/catalog/5690/
・M5Stack TVOC/eCO2ユニット
 https://www.switch-science.com/catalog/6619/
・Grove OLED Display 0.96”
 https://www.switch-science.com/catalog/829/
・Grove Digital Light Sensor
 https://www.switch-science.com/catalog/1174/

以下のGitHubに上げておきました。

poruruba/RemoteArduino
 https://github.com/poruruba/RemoteArduino

MQTTトピック名

2つのMQTTトピックを使います。
1つは、Node.js側からM5StickCに要求するコマンド送信のためのトピック
もう一つは、M5StickCから処理結果をNode.jsに返すためのトピック

とりあえず、
前者は、m5lite/cmd【M5StickCのMacアドレスの16進数表記】、
後者は、m5lite/rsp【M5StickCのMacアドレスの16進数表記】
にしています。

Arduino側

以下のライブラリを使わせていただいています。

tanakamasayuki/ESP32LitePack
 https://github.com/tanakamasayuki/ESP32LitePack

もちろん、それに含まれる以下も利用させていただきました。
lovyan03/LovyanGFX
 https://github.com/lovyan03/LovyanGFX

これらのおかげで、描画系が整備され、マルチデバイスで動くようになりましたっ!!

また、以下も利用させていただいています。

knolleary/pubsubclient
 https://github.com/knolleary/pubsubclient

ArduinoJson
 https://arduinojson.org/

MQTTで受信されるデータフォーマット

こんなJSONがM5StickCに届きます。

{
 “client_id”: “【クライアントID】”,
 “tx_id”: 【トランザクションID】,
 “device_type”: “【デバイスタイプ】”,
 “cmd”: “【コマンド名】”,
 “params”: {
  “param1”: 【1番目のパラメータ】,
  “param2”: 【2番目のパラメータ】,
  “param3”: 【3番目のパラメータ】,
  “param4”: 【4番目のパラメータ】,
  “param5”: 【5番目のパラメータ】,
  “param6”: 【6番目のパラメータ】,
  “param7”: 【7番目のパラメータ】
 }
}

クライアントIDとトランザクションIDは、送信側(Node.js側)が自由に付けて送ります。M5StickC側はそのまま同じ値をレスポンスに返します。トランザクションIDを毎回インクリメントすれば、送ったレスポンスかどうかがわかることになります。
デバイスタイプは今のところ以下の4種類です。

  • Serial
  • Gpio
  • Lcd
  • Wire、Wire1

コマンド名は、デバイスタイプごとにサポートする名前が異なります。
整理する意味でざっと上げてみました。

デバイス名 コマンド名
Serial begin
^ end
^ available
^ read
^ peek
^ flush
^ print
^ println
^ write
^ write_str
^ write_buf
Gpio pinMode
^ digitalWrite
^ digitalRead
^ analogRead
^ analogReadResolution
Wire begin
^ requestFrom
^ beginTransmission
^ endTransmission
^ write
^ write_str
^ write_buf
^ available
^ read
^ read_buf
Lcd setRotation
^ setTextColor
^ setBrightness
^ drawPixel
^ drawLine
^ drawRect
^ fillRect
^ fillScreen
^ drawTriangle
^ fillTriangle
^ drawCircle
^ fillCircle
^ drawEllipse
^ fillEllipse
^ drawBmpData
^ getRange

およそArduinoに合わせてあるので、イメージしやすいかと思います。
M5StickCでは、受け取ったMQTTパケットを解析し、ArduinoのAPI呼び出しに渡す処理をしているだけです。
処理結果は、MQTTトピックにPublishして戻しています。

レスポンスのフォーマットは以下の通りです。

{
 “client_id”: “【クライアントID】”,
 “tx_id”: 【トランザクションID】,
 “device_type”: “【デバイスタイプ】”,
 “rsp”: “【コマンド名】”,
 “status”: “【処理結果】”,
 “params”: {
  “param1”: 【1番目のパラメータ】,
  “param2”: 【2番目のパラメータ】,
  “param3”: 【3番目のパラメータ】,
  “param4”: 【4番目のパラメータ】,
  “param5”: 【5番目のパラメータ】,
  “param6”: 【6番目のパラメータ】,
  “param7”: 【7番目のパラメータ】
 }
}

cmdがrspに代わっただけです。
クライアントIDとトランザクションIDは、コマンドにあったものをそのまま返しています。
処理結果には、”OK”か”NG”が入ります。NGの場合には、reasonも一緒に理由が入って帰ります。
paramsは処理結果です。デバイスタイプとコマンド名で決まる処理内容によって異なります。

複雑な処理はしておらず、ただただ、各コマンドをArduinoのAPIに変換する処理をえんえんと記述しています。

Node.js側

以下のファイル構成になっています。

arduino.js
 これがメインとなるクラスです。これをrequireすると、Serial、Gpio、Lcd、WireおよびWire1が一緒にインスタンス化されます。

arduino_device/Gpio.js
 Gpioのコマンド送信のためのクラスです。

arduino_device/Serial.js
 Serialのコマンド送信のためのクラスです。

arduino_device/Lcd.js
 Lcdのコマンド送信のためのクラスです。

arduino_device/Wire.js
 Wireのコマンド送信のためのクラスです。
 周辺デバイスはI2Cデバイスが多いので、こちらを多用しました。

使い方(Node.js側)

まずは、メインとなるArduinoクラスをインスタンス化します。

index.js
const Arduino = require('./arduino');
const arduino = new Arduino(MQTT_CLIENT_ID, MQTT_HOST_URL, MQTT_TOPIC_CMD, MQTT_TOPIC_RSP);

その際に、接続するMQTTのクライアントID、MQTTブローカのURL、接続するMQTTトピック名の送信用と受信用を指定します。トピック名は、接続するM5StickCの設定に合わせます。

MQTTブローカのURLは以下のような感じに指定します。
 tcp://【MQTTブローカのホスト名】:1883

そして、以下のようにしてMQTTに接続し、内部でLcdの画面サイズを取得しています。

index.js
 await arduino.connect();

あとは、Wireを使いたい場合は、arduino.Wireにあります。

index.js
  var wire = arduino.Wire;
  await wire.begin();

周辺デバイスの制御

手持ちにある、I2Cデバイスの周辺デバイスを一通り実装しました。
I2Cアドレスが違うのであれば、I2Cハブを介して同時に接続できるかと思います。以下では4つ同時につないでいます。SSD 1308のOLED Displayは少々遅いですが、それ以外はあまり遅さは気になりませんかね。

・M5Stack ENVユニット
  device/DHT12.js
  device/BME280.js
・M5Stack TVOC/eCO2ユニット
  device/SGC30.js
・Grove OLED Display 0.96”
  device/SSD1308.js
・Grove Digital Light Sensor
  device/TSL2561.js

M5StackやAdafruitやseeedが提供するサンプルを参考にさせていただきました。っていうより、ほぼそのままNode.jsにポーティングです。
以下のようにして使います。

index.js
const SGC30 = require('./device/SGC30');
const DHT12 = require('./device/DHT12');
const TSL2561 = require('./device/TSL2561');
const BME280 = require('./device/BME280');
const SSD1308 = require('./device/SSD1308');

  var bme280 = new BME280(wire);
  await bme280.begin();
  var ret = await bme280.readTemperature();
  console.log(ret);
  var ret = await bme280.readPressure();
  console.log(ret);

  var dht12 = new DHT12(wire);
  var ret = await dht12.readTemperature();
  console.log(ret);
  var ret = await dht12.readHumidity();
  console.log(ret);

  var tsl2561 = new TSL2561(wire);
  await tsl2561.init();
  var ret = await tsl2561.readVisibleLux();
  console.log(ret);

  var sgc30 = new SGC30(wire);
  var ret = await sgc30.begin();
  console.log(ret);
  await sgc30.IAQmeasure();
  console.log(sgc30.TVOC);
  console.log(sgc30.eCO2);

  var ssd1308 = new SSD1308(wire);
  await ssd1308.init();
  await ssd1308.clear();
  await ssd1308.put_pixel(1, 1, true);
  await ssd1308.update();

終わりに

自分はNode.jsに慣れているので、周辺デバイスのドライバ作成のデバッグが非常にはかどりました。修正・再実行が一瞬(数秒)なので。

ただ、動いたっぽく見えただけなので、いろいろバグがあるかもしれません。
さらに、I2C以外は動作確認していません。ほんと機械的な実装なので。。。

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Obnizと圧電スピーカーで日が暮れたら「ゆうやけこやけ」を流して切なくなってみた

まずは完成形

CdSセル(照度センサ)で暗くなると、ゆうやけこやけが流れます。
この曲聞くと切なくなるな。。

使った部品

・Obniz Board 1Y
・圧電スピーカー(PKM13EPYH4000-A0)
・CdSセル(MI527/MI5527)
・カーボン抵抗 1/2W330Ω(CFS50J330RB)
・ミニブレッドボード(BB-601)
・ブレッドボード・ジャンパーワイヤ(オス-オス)(BBJ-20)

接続図

image.png

コード

.js
const Obniz = require('obniz');
const obniz = new Obniz('0000-0000'); // Obniz_ID

// 任意の秒数待つことができる関数
// 参考: https://qiita.com/suin/items/99aa8641d06b5f819656
const sleep = (msec) => new Promise(res => setTimeout(res, msec));

// 音階
const Key = {
    "" : 261.626,
    "" : 293.665,
    "" : 329.628,
    "ファ" : 349.228,
    "" : 391.995,
    "" : 440.000,
    "" : 493.883,
    "ド2" : 523.251,
    "レ2" : 587.330
}

obniz.onconnect = async function () {

    // ディスプレイ表示
    obniz.display.clear();
    obniz.display.print('TEST');

    // スピーカー
    const speaker = obniz.wired('Speaker', { signal: 0, gnd: 1 });

    // 照度センサ
    obniz.io9.output(true);  // io9電圧を5Vに(電源+)
    obniz.io11.output(false); // io11電圧を0Vに(電源−)

    // setIntervalで一定間隔で処理
    setInterval(async function () {
        // io10をアナログピンに(照度センサーの値を取得)
        var voltage = await obniz.ad10.getWait();
        console.log(`changed to ${voltage} v`);

        if (voltage < 0.3) {
            // 暗くなったら「ゆうやけこやけ」を流す
            speaker.play(Key[""]); await sleep(1000); speaker.stop(); await sleep(100);
            speaker.play(Key[""]); await sleep(500); speaker.stop(); await sleep(100);
            speaker.play(Key[""]); await sleep(500); speaker.stop(); await sleep(100);

            speaker.play(Key[""]); await sleep(500); speaker.stop(); await sleep(100);
            speaker.play(Key[""]); await sleep(500); speaker.stop(); await sleep(100);
            speaker.play(Key[""]); await sleep(500); speaker.stop(); await sleep(100);
            speaker.play(Key[""]); await sleep(500); speaker.stop(); await sleep(100);

            speaker.play(Key[""]); await sleep(500); speaker.stop(); await sleep(100);
            speaker.play(Key[""]); await sleep(500); speaker.stop(); await sleep(100);
            speaker.play(Key[""]); await sleep(500); speaker.stop(); await sleep(100);
            speaker.play(Key[""]); await sleep(500); speaker.stop(); await sleep(100);

            speaker.play(Key[""]); await sleep(1500); speaker.stop(); await sleep(500);

            speaker.play(Key[""]); await sleep(1000); speaker.stop(); await sleep(100);
            speaker.play(Key[""]); await sleep(500); speaker.stop(); await sleep(100);
            speaker.play(Key[""]); await sleep(500); speaker.stop(); await sleep(100);

            speaker.play(Key[""]); await sleep(500); speaker.stop(); await sleep(100);
            speaker.play(Key["ド2"]); await sleep(500); speaker.stop(); await sleep(100);
            speaker.play(Key["ド2"]); await sleep(500); speaker.stop(); await sleep(100);
            speaker.play(Key[""]); await sleep(500); speaker.stop(); await sleep(100);

            speaker.play(Key[""]); await sleep(500); speaker.stop(); await sleep(100);
            speaker.play(Key[""]); await sleep(500); speaker.stop(); await sleep(100);
            speaker.play(Key[""]); await sleep(500); speaker.stop(); await sleep(100);
            speaker.play(Key[""]); await sleep(500); speaker.stop(); await sleep(100);

            speaker.play(Key["ド2"]); await sleep(1500); speaker.stop(); await sleep(500);
        }
    }, 20000);  // 20秒(約1曲分は待つ)
}

やってみた感想

圧電スピーカーは周波数で音階を決められるので、曲を流すのは割と簡単でした。
参考:音階周波数

今回、圧電スピーカーで曲を流してみたかっただけですが、なかなか面白かった。ハマりそうです。。スピーカーを複数繋げれば、和音も表現できそうで曲の幅が広がりそうで、時間があったらやってみたい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GitHub Actions で WebGL2 を動かすまで

capture_.png

概要

WebGL2 を含むプログラムを GitHub Actions の Node.js 環境で実行する現実的な選択肢は二つ

  • Headless Chrome を使って localhost 越しに呼びたい式を渡し、評価された結果を受け取る
  • node-gles を使って WebGL の関数呼び出しを OpenGL ES に内部で変換して呼び出す

どちらも面倒な点はありますが、それぞれ動かすところまで試したのでやり方を書いておきます。


対象読者

  • Node.js を使ったことがある
  • WebGL を使ったことがある

環境

  • Node.js
  • TypeScript
  • Jest

リポジトリ

https://github.com/agehama/webgl2-test

方法1. Headless Chrome を使う

Chrome には画面を立ち上げずにバックグラウンドで動作するヘッドレスモードがあり、プログラムからこれを操作することでテストやキャプチャの撮影などが自動で行えます。

Node.js から Chrome を操作するには次のいずれかのライブラリを使用するのが一般的なようです。

今回はページを開いて式を実行するだけなのでどちらでも大して変わりませんが、より簡単な Puppeteer を使うことにします。

npm install -D puppeteer

実験1.1. WebGL の情報を取得して表示する(Chrome)

まずは WebGL のバージョンや使えるリソースのサイズを取得するプログラムで実験をしてみます。

ソースコード

次の webglSimple() は引数に WebGL2RenderingContext を取るのでこれをどうやって与えるかというのが今回の問題になります。

src/index.ts
export function webglSimple(gl: WebGL2RenderingContext): string
{
    return [
        `------------------------------------------------------------`,
        `gl.RENDERER  | ${gl.getParameter(gl.RENDERER)}`,
        `gl.VERSION   | ${gl.getParameter(gl.VERSION)}`,
        `------------------------------------------------------------`,
        `gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS  | ${gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS)}`,
        `gl.MAX_CUBE_MAP_TEXTURE_SIZE         | ${gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE)}`,
        `gl.MAX_FRAGMENT_UNIFORM_VECTORS      | ${gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS)}`,
        `gl.MAX_TEXTURE_IMAGE_UNITS           | ${gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS)}`,
        `gl.MAX_TEXTURE_SIZE                  | ${gl.getParameter(gl.MAX_TEXTURE_SIZE)}`,
        `gl.MAX_VARYING_VECTORS               | ${gl.getParameter(gl.MAX_VARYING_VECTORS)}`,
        `gl.MAX_VERTEX_ATTRIBS                | ${gl.getParameter(gl.MAX_VERTEX_ATTRIBS)}`,
        `gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS    | ${gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS)}`,
        `gl.MAX_VERTEX_UNIFORM_VECTORS        | ${gl.getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS)}`,
        `------------------------------------------------------------`,
    ].join('\n');
}

この関数を GitHub Actions から呼び出して結果を表示するのがとりあえずの目標です。

ブラウザから使うので HTML も仮で用意しておきます。

public/index.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>webgl2-test</title>
    </head>
    <body>
        <canvas id="gl"></canvas>
        <script src="./index.js"></script>
    </body>
</html>

テストコード

テストでは Puppeteer を使ってブラウザ上で JavaScript を実行します。

次の remoteEval() 関数は puppeteer.launch() で Chrome を立ち上げた後 page.goto() で localhost に接続します。そしてページが読み込まれたら page.evaluate() を呼ぶことでページ上で式を実行してその結果を返します。

test/chrome_helper.ts
const puppeteer = require('puppeteer');

export async function remoteEval(expr: () => any, port: number, headless: boolean, chromeFlags: string[] = [])
{
    return new Promise(async (resolve: (x:any) => void) =>
    {
        const browser = await puppeteer.launch({ headless: headless, args: chromeFlags });
        const page = await browser.newPage();
        await page.goto(`http://localhost:${port}`, { waitUntil: 'domcontentloaded' });

        const result = await page.evaluate(expr);

        await browser.close();
        resolve(result);
    });
}

これを使って src/index.ts で定義した webglSimple() を呼び出すだけのテストを書きます。

次の call_webglSimple() は先ほどの HTML を開いたページ上で実行するため document.querySelector() で canvas を取得することができます。これを通常モードとヘッドレスモードでそれぞれ実行するテストを書きます。

test/chrome_simple.test.ts
import {remoteEval} from "./chrome_helper";

const call_webglSimple = () => eval(`(() =>
{
    const canvas = document.querySelector("canvas");
    const gl = canvas.getContext("webgl2");
    return webglSimple(gl);
})()`);

test("simple (chrome headless)", (async function()
{
    return remoteEval(call_webglSimple, 8080, true).then(
        (result:any) =>
        {
            console.log(result);
            expect(`${result}`).not.toBe("");
        });
}), 60000);

test("simple (chrome browser)", (async function()
{
    return remoteEval(call_webglSimple, 8080, false).then(
        (result:any) =>
        {
            console.log(result);
            expect(`${result}`).not.toBe("");
        });
}), 60000);

GitHub Actions

最後にこのプログラムを実際に実行するワークフローを書きます。run 以外はテンプレをそのまま使いました。ここでは tsc で TypeScript をコンパイルした後 http-server を立ち上げた状態で test を実行しています。

.github/workflows/node.js.yml
#... 略 ...
jobs:
  chrome_simple:
    runs-on: macos-latest
    strategy:
      matrix:
        node-version: [12.x]
    steps:
    - uses: actions/checkout@v2
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}
    - run: |
        npm ci
        npm install -D puppeteer
        npm run tsc
        npm run server ./public -p 8080 &
        npm test -- chrome_simple

実行結果

https://github.com/agehama/webgl2-test/runs/1268400898
simple (chrome headless)
    ------------------------------------------------------------
    gl.RENDERER  | WebKit WebGL
    gl.VERSION   | WebGL 2.0 (OpenGL ES 3.0 Chromium)
    ------------------------------------------------------------
    gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS  | 32
    gl.MAX_CUBE_MAP_TEXTURE_SIZE         | 8192
    gl.MAX_FRAGMENT_UNIFORM_VECTORS      | 261
    gl.MAX_TEXTURE_IMAGE_UNITS           | 16
    gl.MAX_TEXTURE_SIZE                  | 8192
    gl.MAX_VARYING_VECTORS               | 32
    gl.MAX_VERTEX_ATTRIBS                | 32
    gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS    | 16
    gl.MAX_VERTEX_UNIFORM_VECTORS        | 256
    ------------------------------------------------------------

simple (chrome browser)
    ------------------------------------------------------------
    gl.RENDERER  | WebKit WebGL
    gl.VERSION   | WebGL 2.0 (OpenGL ES 3.0 Chromium)
    ------------------------------------------------------------
    gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS  | 80
    gl.MAX_CUBE_MAP_TEXTURE_SIZE         | 16384
    gl.MAX_FRAGMENT_UNIFORM_VECTORS      | 1024
    gl.MAX_TEXTURE_IMAGE_UNITS           | 16
    gl.MAX_TEXTURE_SIZE                  | 16384
    gl.MAX_VARYING_VECTORS               | 32
    gl.MAX_VERTEX_ATTRIBS                | 16
    gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS    | 16
    gl.MAX_VERTEX_UNIFORM_VECTORS        | 1024
    ------------------------------------------------------------

GitHub Actions で動かした出力結果がこちらです。これで WebGL2 が動作することを無事確認できました!

ただし、ここで気になるのはヘッドレスモードのリソースサイズが通常モードより大きく制限されているという点です。このように同じ実行環境でもヘッドレスモードだとグラフィックスの挙動が変わるということは意識しておく必要があります。

実験1.2. Texture3D に MRT を使って描き込む(Chrome)

動作が確認できたので早速 WebGL2 でないと動かないプログラムを試してみましょう。

ソースコード

次の webglTexture3d() は Texture3D に Multiple Render Targets を使って値を描き込み、その結果を gl.readPixels() で読んで配列に詰めて返す関数です。描き込む値はフラグメントシェーダの中で定義しています。

src/index.ts
export function webglTexture3d(gl: WebGL2RenderingContext): Uint8Array[]
{
    const vs = gl.createShader(gl.VERTEX_SHADER) as WebGLShader;
    {
        gl.shaderSource(vs,
`#version 300 es
void main()
{
    vec3[6] vertices = vec3[](
        vec3(-1.0, -1.0, 0),
        vec3(+1.0, -1.0, 0),
        vec3(-1.0, +1.0, 0),
        vec3(-1.0, +1.0, 0),
        vec3(+1.0, -1.0, 0),
        vec3(+1.0, +1.0, 0)
    );
    gl_Position = vec4(vertices[gl_VertexID], 1);
}`
        );
        gl.compileShader(vs);
    }

    const fs = gl.createShader(gl.FRAGMENT_SHADER) as WebGLShader;
    {
        gl.shaderSource(fs, 
`#version 300 es
precision mediump float;
out uvec4 outColor[4];
void main()
{
    uvec2 xy = uvec2(gl_FragCoord.xy);
    outColor[0] = uvec4(xy + uvec2( 0,  0), 0u, 255u);
    outColor[1] = uvec4(xy + uvec2(10, 10), 0u, 255u);
    outColor[2] = uvec4(xy + uvec2(20, 20), 0u, 255u);
    outColor[3] = uvec4(xy + uvec2(30, 30), 0u, 255u);
}`
        );
        gl.compileShader(fs);
    }

    const program = gl.createProgram() as WebGLProgram;
    gl.attachShader(program, vs);
    gl.attachShader(program, fs);
    gl.linkProgram(program);
    gl.useProgram(program);

    const width = 2;
    const height = 2;
    const depth = 4;
    const texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_3D, texture);
    gl.texImage3D(gl.TEXTURE_3D, 0, gl.RGBA8UI, width, height, depth, 0, gl.RGBA_INTEGER, gl.UNSIGNED_BYTE, null);

    const fb = gl.createFramebuffer();
    gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
    for(let z = 0; z < 4; z++)
    {
        gl.framebufferTextureLayer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + z, texture, 0, z);
    }
    gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1, gl.COLOR_ATTACHMENT2, gl.COLOR_ATTACHMENT3]);

    gl.viewport(0, 0, width, height);
    gl.drawArrays(gl.TRIANGLES, 0, 6);

    let results = [];
    const pixels = new Uint8Array(4*width*height);
    for(let z = 0; z < 4; z++)
    {
        gl.readBuffer(gl.COLOR_ATTACHMENT0 + z);
        gl.readPixels(0, 0, width, height, gl.RGBA_INTEGER, gl.UNSIGNED_BYTE, pixels);
        results.push(pixels.slice());
    }

    return results;
}

テストコード

こちらも通常モードとヘッドレスモードでそれぞれ webglTexture3d() を呼び出すテストを書きます。

test/chrome_texture3d.test.ts
import {remoteEval} from "./chrome_helper";

const call_webglTexture3d = () => eval(`(() =>
{
    const canvas = document.querySelector("canvas");
    const gl = canvas.getContext("webgl2");
    return webglTexture3d(gl);
})()`);

test("texture3d (chrome headless)", (async function()
{
    return remoteEval(call_webglTexture3d, 8080, true).then(
        (result:any) =>
        {
            console.log(result);
            expect(`${result}`).not.toBe([]);
        });
}), 60000);

test("texture3d (chrome browser)", (async function()
{
    return remoteEval(call_webglTexture3d, 8080, false).then(
        (result:any) =>
        {
            console.log(result);
            expect(`${result}`).not.toBe([]);
        });
}), 60000);

GitHub Actions

ワークフローも 実験1.1 と同じで、呼び出す test だけ変えます。

.github/workflows/node.js.yml
jobs:
  #... 略 ...
  chrome_texture3d:
    runs-on: macos-latest
    strategy:
      matrix:
        node-version: [12.x]
    steps:
    - uses: actions/checkout@v2
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}
    - run: |
        npm ci
        npm install -D puppeteer
        npm run tsc
        npm run server ./public -p 8080 &
        npm test -- chrome_texture3d

実行結果

https://github.com/agehama/webgl2-test/runs/1268400887
// texture3d (chrome headless)
[
    { '0':   1, '1':   0,  '2':   0,  '3':   0,  '4':   0,  '5':   0,  '6':   0,  '7':   0,
      '8':   0, '9':   0, '10':   0, '11':   0, '12': 112, '13': 114, '14': 101, '15': 115 },
    { '0':  35, '1': 118,  '2': 101,  '3': 114,  '4': 115,  '5': 105,  '6': 111,  '7': 110,
      '8':  32, '9':  51, '10':  48, '11':  48, '12':  32, '13': 101, '14': 115, '15':  10 },
    { '0': 112, '1': 114,  '2': 101,  '3':  99,  '4': 105,  '5': 115,  '6': 105,  '7': 111,
      '8': 110, '9':  32, '10': 109, '11': 101, '12': 100, '13': 105, '14': 117, '15': 109 },
    { '0': 112, '1':  32,  '2': 102,  '3': 108,  '4': 111,  '5':  97,  '6': 116,  '7':  59,
      '8':  10, '9': 111, '10': 117, '11': 116, '12':  32, '13': 117, '14': 118, '15': 101 }
]

// texture3d (chrome browser)
[
    { '0':  0, '1':  0,  '2': 0,  '3': 255,  '4':  1,  '5':  0,  '6': 0,  '7': 255,
      '8':  0, '9':  1, '10': 0, '11': 255, '12':  1, '13':  1, '14': 0, '15': 255 },
    { '0': 10, '1': 10,  '2': 0,  '3': 255,  '4': 11,  '5': 10,  '6': 0,  '7': 255,
      '8': 10, '9': 11, '10': 0, '11': 255, '12': 11, '13': 11, '14': 0, '15': 255 },
    { '0': 20, '1': 20,  '2': 0,  '3': 255,  '4': 21,  '5': 20,  '6': 0,  '7': 255,
      '8': 20, '9': 21, '10': 0, '11': 255, '12': 21, '13': 21, '14': 0, '15': 255 },
    { '0': 30, '1': 30,  '2': 0,  '3': 255,  '4': 31,  '5': 30,  '6': 0,  '7': 255,
      '8': 30, '9': 31, '10': 0, '11': 255, '12': 31, '13': 31, '14': 0, '15': 255 }
]

実行するとなんと二つの出力が全然異なる結果になってしまいました。これはヘッドレスモードの方が間違っており通常モードでは正しい結果を出力できています。ヘッドレスモードの実行結果はランダムっぽい値が入っているときと全て0埋めされているときがあるので、もはや何も描画できていないのかもしれません。

そしてもう一つ気になるのが、関数からは Uint8Array[] 型で結果を返しているのに受け取った結果は JSON に変換されているという点です。この理由はシンプルで、テストを行う Node.js と式を実行する Chrome はそれぞれ別々に JavaScript 実行環境を動かしており、両者の間でデータを受け渡す手段が文字列しかないためです。この変換は Puppeteer が内部で自動的に行っているようで、chrome-remote-interface だと容赦なく undefined が返って来るため事前に JSON.stringify() などに通して文字列にした状態で返す必要があります。

自分の想定では複雑なシェーダについて CPU で同じ計算を行う関数を書いておき、シェーダの計算結果と照らし合わせるテストをしたいと思っていたので途中でこういった変換を挟まざるを得ないというのはちょっと微妙だなあという感想でした。まあテストまで全部 Chrome 上で行ってしまえばこれは問題になりませんが…

いずれにせよ WebGL の単体テストを行うという用途に対しては、この方法だと少し大がかり過ぎてあまり向いていない気がしました。しかしその分 Chrome での動作が保証できるという強みはあるので、厳密なデータ比較まで行わなくてもとりあえず動かすだけ動かしておくというのが良いのかもしれません。

方法2. node-gles を使う

https://github.com/google/node-gles

node-gles は WebGL から OpenGL ES へのバインディングを提供するライブラリで、これ自身は内部で ANGLE を呼び出します。同じコンセプトで広く使われている headless-gl から WebGL2 に対応する気配がないため TensorFlow.js の中の人によって新たに開発されています。

しかしながら、2020年10月現在ではまだ WebGL2 の関数バインディングはほとんど入っていません。バインディングが少ない理由については一応説明があり、将来的に Khronos の IDL ファイルから自動生成する予定でとりあえず使うものしか入れていない(https://github.com/google/node-gles/issues/1#issuecomment-435165176) とのことです。ということで今はかなり作り途中のものを無理やり使っていくことになります。

ここで大事なのはバインディングさえ追加すれば WebGL 2.0 ( ES 3.0 ) の関数を呼び出せることで、追加するだけならそれほど難しくはないので現時点でも実用できなくはないということです。また、GitHub Actions の windows-latest と ubuntu-latest では残念ながら動きませんでしたが、macos-latest では動くことが確認できたのでひとまずこれで使ってみることにします。

npm install -D node-gles

実験2.1. WebGL の情報を取得して表示する(node-gles)

使うソースコードは 実験1.1 と同じ webglSimple() 関数なので省略します。ちなみに webglSimple() は現在の node-gles が対応している範囲に収まっている(収めた)ためそのまま実行することができます。

テストコード

nodeGles.createWebGLRenderingContext() で WebGL と同じ API にアクセスできるオブジェクトが返ってくるので、これをそのまま webglSimple() に渡して結果を取得します。

test/node_gles_simple.test.ts
const nodeGles = require('node-gles');
import {webglSimple} from "../src/index";

test("simple (node-gles)", () =>
{
    const gl = nodeGles.createWebGLRenderingContext();
    const result = webglSimple(gl);
    console.log(result);
    expect(result).not.toBe([]);
});

GitHub Actions

ワークフローは 実験1.1 とほぼ同じです。

.github/workflows/node.js.yml
jobs:
  #... 略 ...
  node_gles_simple:
    runs-on: macos-latest
    strategy:
      matrix:
        node-version: [12.x]
    steps:
    - uses: actions/checkout@v2
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}
    - run: |
        npm ci
        npm install -D node-gles
        npm test -- node_gles_simple

実行結果

https://github.com/agehama/webgl2-test/runs/1268400898
simple (node-gles)
    ------------------------------------------------------------
    gl.RENDERER  | ANGLE (Apple Inc., Apple Software Renderer, OpenGL 4.1 core)
    gl.VERSION   | OpenGL ES 3.0 (ANGLE 2.1.0.9512a0ef062a)
    ------------------------------------------------------------
    gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS  | 32
    gl.MAX_CUBE_MAP_TEXTURE_SIZE         | 16384
    gl.MAX_FRAGMENT_UNIFORM_VECTORS      | 1024
    gl.MAX_TEXTURE_IMAGE_UNITS           | 16
    gl.MAX_TEXTURE_SIZE                  | 16384
    gl.MAX_VARYING_VECTORS               | 32
    gl.MAX_VERTEX_ATTRIBS                | 16
    gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS    | 16
    gl.MAX_VERTEX_UNIFORM_VECTORS        | 1024
    ------------------------------------------------------------

node-gles でも無事に動作が確認できました。

実験1.1gl.RENDERER の情報を取得したときは WebKit としか返してくれませんでしたが、今回は ANGLE が中で実際に呼んでいるグラフィックス API の情報を取得できていますね。Software Renderer と書いてあるのが若干気になりますが、まあこれは仮想環境なのでしょうがないのかもしれません。よくわかりませんが。

とりあえず動くことが確認できたので先に進むことにします。

実験2.2. Texture3D に MRT を使って描き込む(node-gles)

次に 実験1.2 で書いた webglTexture3d() をこちらでも動かしてみます。

ここで問題になるのが webglTexture3d() の中でバインディングが足りない関数がいくつかあることです。具体的には texImage3D(), framebufferTextureLayer(), drawBuffers(), readBuffer() とあと他にいくつかの定数が足りていないのでこれらを以下に追加していきます。

https://github.com/agehama/node-gles

バインディングの追加

必要になるのは基本的に次の三つです

  1. egl_context_wrapper.h(.cc) に呼び出したい GLES の関数ポインタを取得してメンバに持っておく
  2. webgl_rendering_context.h(.cc) に WebGL から受け取った引数を取り出して取得した関数ポインタに渡して呼び出す関数を定義する
  3. webgl_rendering_context.cc に 2. で定義した関数に NAPI_DEFINE_METHOD() で名前を付けて Node.js に公開する

あとはnpm install を呼ぶと自動でコンパイルが走るようなので、これで build/Release/nodejs_gl_binding.node が生成されて勝手に使えるようになります。

今回追加した差分はこちらから確認できます。
https://github.com/agehama/node-gles/compare/b4cb488...47d2d00

テストコード

実験2.1 と大体同じですがこちらは nodeGles のパスだけ fork 版を呼ぶために変更しています。

node_gles_texture3d.test.ts
const nodeGles = require('../temp/node-gles/src/index');
import {webglTexture3d} from "../src/index";

test("texture3d (node-gles)", () =>
{
    const gl = nodeGles.createWebGLRenderingContext();
    const result = webglTexture3d(gl);
    console.log(result);
    expect(result).not.toBe([]);
});

GitHub Actions

ワークフローにも fork 版の node-gles を使用するために、npm からではなく git から clone してインストールするという手順を加えています。

.github/workflows/node.js.yml
jobs:
  #... 略 ...
  node_gles_texture3d:
    runs-on: macos-latest
    strategy:
      matrix:
        node-version: [12.x]
    steps:
    - uses: actions/checkout@v2
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}
    - run: |
        cd temp
        git clone https://github.com/agehama/node-gles.git
        cd node-gles
        npm install
        cd ../../
        npm ci
        npm test -- node_gles_texture3d

実行結果

https://github.com/agehama/webgl2-test/runs/1268400898
texture3d (node-gles)
    [
      Uint8Array(16) [
        0, 0, 0, 255, 1, 0, 0, 255,
        0, 1, 0, 255, 1, 1, 0, 255
      ],
      Uint8Array(16) [
        10, 10, 0, 255, 11, 10, 0, 255,
        10, 11, 0, 255, 11, 11, 0, 255
      ],
      Uint8Array(16) [
        20, 20, 0, 255, 21, 20, 0, 255,
        20, 21, 0, 255, 21, 21, 0, 255
      ],
      Uint8Array(16) [
        30, 30, 0, 255, 31, 30, 0, 255,
        30, 31, 0, 255, 31, 31, 0, 255
      ]
    ]

こちらが実行結果です。今度は完全に期待通りの出力が得られました!!嬉しい!

ということでバインディングを追加するという手間さえ無ければ文句なくお勧めできるのですが、現状だと実験的なプロジェクトとかでない限りまだ気軽には使いにくそうだなと感じました。

また、ブラウザの WebGL との挙動の違いについてですが、node-gles だと例えばシェーダが使っていない uniform 変数へ値をセットしようとするとセグフォで落ちてしまいます。ブラウザだと仮に不正な入力があってもそのまま投げて落ちないよう入念にチェックしていると思うのでそこは大きく異なる点だと思います。グラフィックス部分に限れば ANGLE に投げているだけなのでブラウザと描画結果が変わるみたいなことはあまり起きにくいんじゃないかなあと思います。

結論

しばらくは node-gles の成長を待ちつつ使いたい関数を自分で追加していくのが良いかなと思いました。それはそれとして Puppeteer でページを表示するだけなら簡単だったので headless: false でとりあえず動かしてキャプチャでも撮っておくのが良さそうでした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

EC2で古いバージョンのnode.jsを最新バージョンにする

ポートフォリオの作成がひと段落ついたのでAWSにデプロイしようとしたところ、node.jsの設定でどハマりしたので皆さんに情報共有していこうと思います。

事象

以下の記事を参考にAWSで通常デプロイに挑戦しました。

【画像付きで丁寧に解説】AWS(EC2)にRailsアプリをイチから上げる方法↓
https://qiita.com/Yuki_Nagaoka/items/975b7598806d6ae0c0b2

こちらの記事を参考にRailsアプリの公開編まではスムーズにいきアプリの公開をしてみたのですがurlを入力してみたところ「このサイトにアクセスできません」と表示され・・・

ログを調べてみると

EC2インスタンスに繋がった状態で以下のコマンドを実行しログファイルを参照

EC2.
[daiki@ip-10-0-0-56 log]$ tail -n 30 production.log
F, [2020-10-12T13:28:37.845307 #22722] FATAL -- : [66518e09-a517-412e-8f3c-97c588a4b2f1] ActionView::Template::Error (The asset "homes.css" is not present in the asset pipeline.
):
F, [2020-10-12T13:28:37.845424 #22722] FATAL -- : [66518e09-a517-412e-8f3c-97c588a4b2f1]     1: <% content_for :css do %>
[66518e09-a517-412e-8f3c-97c588a4b2f1]     2:   <%= stylesheet_link_tag 'homes' %>
[66518e09-a517-412e-8f3c-97c588a4b2f1]     3: <% end %>
[66518e09-a517-412e-8f3c-97c588a4b2f1]     4:   <%= render 'shared/flash_messages'%>
[66518e09-a517-412e-8f3c-97c588a4b2f1]     5: 
F, [2020-10-12T13:28:37.845450 #22722] FATAL -- : [66518e09-a517-412e-8f3c-97c588a4b2f1]   
F, [2020-10-12T13:28:37.845472 #22722] FATAL -- : [66518e09-a517-412e-8f3c-97c588a4b2f1] app/views/homes/index.html.erb:2:in `block in _app_views_homes_index_html_erb__1348361279455874858_75120'
[66518e09-a517-412e-8f3c-97c588a4b2f1] app/views/homes/index.html.erb:1:in `_app_views_homes_index_html_erb__1348361279455874858_75120'
I, [2020-10-12T13:38:07.919149 #22721]  INFO -- : [385da9dc-780c-4a4b-93ea-77683c0f5809] Started GET "/" for 126.11.108.222 at 2020-10-12 13:38:07 +0000
I, [2020-10-12T13:38:07.919848 #22721]  INFO -- : [385da9dc-780c-4a4b-93ea-77683c0f5809] Processing by HomesController#index as HTML
D, [2020-10-12T13:38:07.956165 #22721] DEBUG -- : [385da9dc-780c-4a4b-93ea-77683c0f5809]    (2.8ms)  SET NAMES utf8mb4 COLLATE utf8mb4_general_ci,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
D, [2020-10-12T13:38:07.962259 #22721] DEBUG -- : [385da9dc-780c-4a4b-93ea-77683c0f5809]   Pcrpositive Load (3.0ms)  SELECT  `pcrpositives`.* FROM `pcrpositives` ORDER BY `pcrpositives`.`id` DESC LIMIT 1
D, [2020-10-12T13:38:07.965472 #22721] DEBUG -- : [385da9dc-780c-4a4b-93ea-77683c0f5809]   Pcrtested Load (2.8ms)  SELECT  `pcrtesteds`.* FROM `pcrtesteds` ORDER BY `pcrtesteds`.`id` DESC LIMIT 1
D, [2020-10-12T13:38:07.968633 #22721] DEBUG -- : [385da9dc-780c-4a4b-93ea-77683c0f5809]   Recovery Load (2.8ms)  SELECT  `recoveries`.* FROM `recoveries` ORDER BY `recoveries`.`id` DESC LIMIT 1
I, [2020-10-12T13:38:07.969160 #22721]  INFO -- : [385da9dc-780c-4a4b-93ea-77683c0f5809]   Rendering homes/index.html.erb within layouts/application
I, [2020-10-12T13:38:07.969980 #22721]  INFO -- : [385da9dc-780c-4a4b-93ea-77683c0f5809]   Rendered homes/index.html.erb within layouts/application (0.8ms)
I, [2020-10-12T13:38:07.970113 #22721]  INFO -- : [385da9dc-780c-4a4b-93ea-77683c0f5809] Completed 500 Internal Server Error in 50ms (ActiveRecord: 11.4ms)
F, [2020-10-12T13:38:07.970845 #22721] FATAL -- : [385da9dc-780c-4a4b-93ea-77683c0f5809]   
F, [2020-10-12T13:38:07.970881 #22721] FATAL -- : [385da9dc-780c-4a4b-93ea-77683c0f5809] ActionView::Template::Error (The asset "homes.css" is not present in the asset pipeline.
):
F, [2020-10-12T13:38:07.971160 #22721] FATAL -- : [385da9dc-780c-4a4b-93ea-77683c0f5809]     1: <% content_for :css do %>
[385da9dc-780c-4a4b-93ea-77683c0f5809]     2:   <%= stylesheet_link_tag 'homes' %>
[385da9dc-780c-4a4b-93ea-77683c0f5809]     3: <% end %>
[385da9dc-780c-4a4b-93ea-77683c0f5809]     4:   <%= render 'shared/flash_messages'%>
[385da9dc-780c-4a4b-93ea-77683c0f5809]     5: 
F, [2020-10-12T13:38:07.971197 #22721] FATAL -- : [385da9dc-780c-4a4b-93ea-77683c0f5809]   
F, [2020-10-12T13:38:07.971221 #22721] FATAL -- : [385da9dc-780c-4a4b-93ea-77683c0f5809] app/views/homes/index.html.erb:2:in `block in _app_views_homes_index_html_erb__1348361279455874858_75120'
[385da9dc-780c-4a4b-93ea-77683c0f5809] app/views/homes/index.html.erb:1:in `_app_views_homes_index_html_erb__1348361279455874858_75120'
[daiki@ip-10-0-0-56 log]$ 

なんかasset piipelineで落ちている??
と思いもう一度、Railsアプリをプリコンパイルしてみる。

EC2.
[daiki@ip-10-0-0-56 SONAERU_APP]$ bundle exec rake assets:precompile RAILS_ENV=production
Error: ENOTDIR: not a directory, open '/home/daiki/.config/yarn'
    at Error (native)
    at Object.fs.openSync (fs.js:642:18)
    at fs.readFileSync (fs.js:510:33)
    at /usr/lib/node_modules/yarn/lib/cli.js:100892:58
    at Array.map (native)
    at parseRcPaths (/usr/lib/node_modules/yarn/lib/cli.js:100890:78)
    at Object.findRc (/usr/lib/node_modules/yarn/lib/cli.js:100904:10)
    at getRcConfigForCwd (/usr/lib/node_modules/yarn/lib/cli.js:56916:74)
    at /usr/lib/node_modules/yarn/lib/cli.js:92255:56
    at next (native)
rake aborted!
Autoprefixer doesn't support Node v6.17.1. Update it.
/home/daiki/.rbenv/versions/2.7.0/bin/bundle:23:in `load'
/home/daiki/.rbenv/versions/2.7.0/bin/bundle:23:in `<main>'
Tasks: TOP => assets:precompile
(See full trace by running task with --trace)

ん〜下から5行目あたりでnode.jsが古いと怒られていますね。
ということでnode.jsを最新版にしてみましょう!

以下の記事を参考にしました!

↓yumでのnodejsのバージョンアップにはまった話と解決方法
https://qiita.com/robitan/items/a684a81214767c21a560

手順

まずは下記のコードを実行してroot状態にしておく

EC2.
 $  sudo su -

現在のnode.jsのバージョンを確認

EC2.
 $  node -v
    v6.12.3

node.jsのrpmを確認。2レコード出てきたら古いほうが優先されるのが原因みたいです。

EC2.
 $  ll /etc/yum.repos.d/ | grep node
    -rw-r--r--. 1 root root 472 Oct 21  2016 nodesource-el6.repo
    -rw-r--r--. 1 root root 472 Apr 26  2016 nodesource-el.repo

よって、古いほうのrpmを削除してします。

EC2.
$   rm /etc/yum.repos.d/nodesource-el.repo

一旦、yumをクリーンするコマンドを実行

EC2.
$   yum clean all

再びインストールしなおします。

EC2.
$   yum -y install nodejs

バージョンを確認

EC2.
$  $ node -v
   v12.19.0

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.jsでTop-Level Awaitを試す

Node.jsでTop-Level Awaitがサポートされ(て)たので、非同期通信と言えばなaxiosで試してみます。

Top-Level Await

今まではawaitを利用する際に、async関数内じゃないと使えませんでしたが、async関数を宣言せずにawaitを使えるようになります。

v14.3.0でサポート、v14.8.0でフラグなし

Top-Level AwaitはNode.js v14.3.0でサポートされましたが、この時点だと--experimental-top-level-awaitのフラグを付けて実行する必要がありました。

v14.8.0以降でフラグ無しで利用できます。

axiosで利用してみる

(一応)今回試した環境はNode.js v14.14.0です。

package.jsonに"type": "module"を追記して利用できます。
また、拡張子をmjsにするだけでも利用できます。↓ではmjsで試してみます。

$ npm init -y
$ npm i axios
  • app.mjsを作成

ES Modules形式でimportします。

app.mjs
import axios from 'axios';

const res = await axios.get(`https://protoout.studio`);
console.log(res.data);

めちゃシンプルに書けますね。

  • 実行

実行も(.jsではなく).mjsのファイルを実行します。

$ node app.mjs 

参考: Top-Level Await support in Node.js v14.3.0

補足: 今までの書き方

今までだと、CommonJS 形式でモジュールを読み込み、async関数の中でawait呼び出しをするというのが通常だったと思います。

app.js
'use strict;'

const axios = require('axios');

(async () => {
    const res = await axios.get('https://protoout.studio');
    console.log(res.data);
})();

あと'use strict;'の表記もありますね。ESM形式だとStrictモードがデフォルトで有効なので省略できてます。

こちらは実行は通常通り。

$ node app.js 

所感

ちょっとしたことを試す時にasync関数を書くのは結構めんどくさかったので、Top-Level Awaitはありがたいですね。
Common JS(require)からES Modules(impot/from)への移行の流れもあるのでちょっとしたところから慣れていきたい。

.jsを使わずに.mjsを基本とする流れでも良いのかな...? この辺気になります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初心者】そろそろ適当に npm install するのを卒業したい人へ (><)

はじめに

ネットに転がっている記事などで npm install コマンドを実行するとき、-g--save--save-dev のオプションを指定したり、指定しなかったり、そのときによくわかんないけど作成される node_modulespackage.jsonpackage-lock.json とか毎回調べていたので、備忘録を兼ねてまとめたいと思います。

動作環境

# Node.js のバージョン確認
$ node -v
v14.13.1

# npm のバージョン確認
$ npm -v
6.14.8

Node.js のバージョン管理

Node.js 自体のバージョン管理は nodebrewnodenv などがありますが、お好みでいいと思います。

nodenv はディレクトリごとにバージョンを指定することが可能ですが、nodebrew でも必要に応じてバージョンを切り替えることができるので、そこまで困ることはありませんでした。

初心者であれば nodebrew をお勧めします。
nodebrew で困ることが発生してから nodenv に乗り換えるということでいいと思います。

(参考)

MacにNode.jsをnodebrewでインストールして環境構築【決定版】

MacにNode.jsをインストール

MacにNode.jsをインストール(anyenv + nodenv編)

npm のアップデート

npm 自体のアップデートは以下のコマンドになります。
基本的にバージョンを指定する必要はなく最新でいいと思います。

# npm のアップデート
$ npm update -g npm

※ npm はメジャーバージョンが異なるとかなり挙動が異なるので参照する記事の npm のバージョンにも注意が必要です。

基本的な用語の説明(わかる人は飛ばしてください)

Node.js とは

Node.js とはサーバサイドで動く JavaScript のことです。

npm とは

npm(Node Package Manager)とは Node.js のパッケージ(Package)を管理する(Manager)ツールです。

Node.jsのパッケージ(Package)とは予め用意された便利な機能(各種フレームワークやライブラリ)をまとめたものです。

package.json とは

package.json とは Node.js ベースの JavaScript アプリ開発において、自身のパッケージ(= プロジェクトそのもの)を管理するために使われるファイルのことです。

package.json は touch コマンドなどでも作成できますが、基本的には npm init とコマンド実行して作成します。

# 何もない空のディレクトリ
$ ls
(標準出力なし)

# npm init による package.json の作成(質問をされるが一旦すべて Enter で通過する = npm init -y コマンド実行時と同じ挙動)
$ npm init


# package.json が作成されている
$ ls
package.json
package.json
{
  "name": "<current directory name>",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

package.json にはいろいろ項目がありますが name, version, description, keywords, author, license などのデータは単なる自身のパッケージ(= プロジェクトそのもの)のメタデータであり、自身のパッケージ(= プロジェクトそのもの)を公開するつもりがないならばあまり気にする必要はありません。

調べるといろいろ出てくるのですが、dependencies, devDependencies, scripts, config の4つぐらいを抑えれば、あとは必要に応じて設定すれば問題ないかと思います。

(参考)
package.jsonの構造

【脱線】 npm init しないとどうなる?

npm init も package.json も作成せずにいきなり npm install したらどうなるのか気になったので、以下の2通りで試してみました。

  • npm init せずに npm install した場合
  • npm init せずに npm install <package> した場合

結論から先にいうと npm init しなくても npm install に失敗するわけでもないし、パッケージを指定すれば node_modules が作成されてパッケージのインストールにも成功しました。

しかし、package.json がないとどのパッケージをインストールしたかの記述がどこにも記されないので、node_modules の内容を管理するのは難しくなります。
(package-lock.json は作成されますが、package-lock.json にはインストールしたパッケージの結果のみが記されて、パッケージの依存関係まではわからないので package.json はソースを管理する上で必要になります。)

npm init せずに npm install した場合

# 何もない空のディレクトリ
$ ls
(標準出力なし)

# npm install の実行
$ npm install

# package-lock.json のみが作成される(package.json は作成されない)
$ ls
package-lock.json

# 中身は lockfileVersion のみの記述でパッケージ情報はありませんでした(当たり前)
$ cat package-lock.json 
{
  "lockfileVersion": 1
}

npm init せずに npm install <package> した場合

# 何もない空のディレクトリ
$ ls
(標準出力なし)

# cowsay パッケージをインストール
$ npm install cowsay  
+ cowsay@1.4.0

# package-lock.json と node_modules が作成されるが package.json がない
$ ls
node_modules
package-lock.json

npm と package.json の関係

公開されているパッケージ(ライブラリやフレームワークなど)は npm コマンドによってインストールすることができます。

package.json が存在するディレクトリで npm コマンドによってパッケージをインストールすると、自動的に package.json が更新されます。

npm コマンド実施によって更新される項目は dependenciesdevDependencies という項目なります。

また、パッケージの管理の情報(dependenciesdevDependencies の項目)について、人間が package.json を直接編集することはありません。

すべて npm コマンド経由で更新します。(大事)

node_modules とは

node_modules とは package.json や package-lock.json を元にしてインストールされる各種パッケージのがインストールされるディレクトリのことです。

実質的には package-lock.json に記載されているバージョンのパッケージがインストールされています。

また、package.json さえあれば、npm install コマンドの実行によって node_modules が生成することができるため、通常 .gitignore に指定されるディレクトリです。

node_modules はいろんなパッケージがインストールされているため、とても容量の大きなディレクトリになるので、その管理を package.json や package-lock.json にまかせて node_modules 自体の管理は git で管理対象外にするケースがほとんどです。

npm installnpm install <package> の違い

  • npm install (パッケージの引数なし)
    • package.json の dependenciesdevDependencies に記述されているパッケージ情報を元に node_modules にパッケージをインストールする
  • npm install <package> (パッケージの引数あり)
    • package.json の dependenciesdevDependencies の項目に引数に指定されたパッケージを記述する
    • package.json の dependenciesdevDependencies に記述されているパッケージ情報を元に node_modules にパッケージをインストールする

ポイントは引数にパッケージを指定すると package.json に引数のパッケージが追記されることと、どちらも node_modules にパッケージをインストールすることです。

グローバルインストール と ローカルインストールの違い

npm インストールは「グローバルインストール」と「ローカルインストール」の2種類あります。
ちなみに「グローバルインストール」と「ローカルインストール」の両方とも自身のPCの環境へのインストールのことです。

イメージとしては以下のとおりです。

  • ローカルインストール:node_modules と同じディレクトリにある場合にパッケージ(コマンド)が実行できる
  • グローバルインストール:自身の PC の環境ならどこでもインストールしたパッケージ(コマンド)が実行できる

ただし、グローバルインストールしたからといっても nodebrew などで複数の Node.js のバージョンを管理している場合は、それぞれのバージョンで npm install -g <package> したときのパッケージがそれぞれ別のものとして管理されるので注意が必要です。

package.json
{
  "name": "",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
# 適当なパッケージ(cowsay)をインストール
$ npm install cowsay
+ cowsay@1.4.0

# node_modules と package-lock.json が追加される
$ ls
node_modules
package-lock.json
package.json

# dependencies の項目に cowsay が追加されていることが確認できる
$ cat package.json
{
  (省略)
+ "dependencies": {
+   "cowsay": "^1.4.0"
+ },
  (省略)
}
# 適当なパッケージ(typescript)を -D のオプションをつけてインストール
$ npm install -D typescript
+ typescript@4.0.3

# devDependencies の項目の typescript が追加されていることが確認できる
$ cat package.json
{
  (省略)
  "dependencies": {
    "cowsay": "^1.4.0"
  },
+ "devDependencies": {
+   "typescript": "^4.0.3"
+ },
  (省略)
}

# さらにパッケージを追加してみる
$ npm install webpack
+ webpack@5.1.0

# dependencies の項目に webpack が追加されていることが確認できる
$ cat package.json
{
  (省略)
  "dependencies": {
    "cowsay": "^1.4.0",
+   "webpack": "^5.1.0"
  },
  "devDependencies": {
    "typescript": "^4.0.3"
  },
  (省略)
}

グローバルインストール と ローカルインストールの実行コマンド(npm install)

グローバルインストールされたパッケージとローカルインストールされたパッケージの確認は以下のようになります。

# (前提その1)nodebrew で複数のバージョンの Node.js のバージョンを管理している状態(v14.13.1 を使用)
$ nodebrew ls
v12.6.0
v14.13.1

current: v14.13.1

# グローバルインストール先の確認(npm list -g | head -1)
$ npm list -g | head -1
/Users/sugurutakahashi/.nodebrew/node/v14.13.1/lib

# グローバルインストールされたパッケージの確認(npm list -g --depth=0)
$ npm list -g --depth=0
/Users/sugurutakahashi/.nodebrew/node/v14.13.1/lib
├── npm@6.14.8
├── typescript@4.0.3
└── yarn@1.22.10

# v12.6.0 に切り替え
$ nodebrew use v12.6.0
$ nodebrew ls
v12.6.0
v14.13.1

current: v12.6.0

# グローバルインストール先の確認
$ npm list -g | head -1
/Users/sugurutakahashi/.nodebrew/node/v12.6.0/lib

# グローバルインストールされたパッケージの確認
$ npm list -g --depth=0
/Users/sugurutakahashi/.nodebrew/node/v12.6.0/lib
├── @vue/cli@4.4.6
├── @vue/cli-service-global@4.4.6
├── express@4.17.1
├── firebase-tools@8.6.0
├── gatsby-cli@2.12.66
├── gitbook-cli@2.3.2
├── multi-file-swagger@2.3.0
├── npm@6.14.8
└── yarn@1.22.10

このように Node.js のバージョンを切り替えるとグローバルインストールされたパッケージの内容が異なることがわかります。

# typescript のインストール
$ npm install typescript
+ typescript@4.0.3

# node_modules に typescript のパッケージが存在していることの確認
$ ls node_modules
typescript

# ローカルインストールされたパッケージの確認
$ npm list --depth=0
typescript-node-base@1.0.0 /Users/sugurutakahashi/git/typescript-node-base
└── typescript@4.0.3

# node_modules のない適当なディレクトリに移動
$ cd ../
$ ls node_modules
標準出力なし(= node_modules が空)

# ローカルインストールされたパッケージの確認
$ npm list --depth=0
/Users/sugurutakahashi/git
└── (empty)

このようにローカルインストールされたパッケージはカレントディレクトリの node_modules の内容を確認していることがわかります。

グローバルインストール VS ローカルインストール

では、パッケージをインストールする場合、グローバルインストールローカルインストール のどちらにインストールすべきでしょうか?
基本的には ローカルインストール を選択すべきです。

理由としては、ローカルインストール時に package.json というファイルが作られ、package.json が存在するディレクトリで npm install コマンドを実行するとそのディレクトリにパッケージがインストールされて、インストールされたパッケージが環境に依存しないようにするためです。

グローバルインストールされたパッケージは環境が異なると使用することができません。
例えば、PC を買い替えたらそのパッケージを使用することができないですし、他人の PC でもそのパッケージを使用することができません。

そいういうときにローカルインストールで作成される package.json だけを共有すれば、どの環境でも npm install を実行するだけで、同じパッケージを使用することができます。

ローカルインストールしたパッケージをコマンド実行する

例えば、cowsay というパッケージを npm install cowsay でローカルインストールをした場合、いきなり $ cowsay とコマンドを実行することができません。

$ cowsay と実行すると、以下のように command not found: cowsay と怒られます。

# cowsay パッケージのローカルインストール
$ npm install cowsay
+ cowsay@1.4.0

# ローカルインストールされたパッケージの確認
$ npm list --depth=0
typescript-node-base@1.0.0 /Users/sugurutakahashi/git/typescript-node-base
└── cowsay@1.4.0

# cowsay コマンドの実行(パスが通っていないのでエラーになる)
$ cowsay "hoge"
zsh: command not found: cowsay

ローカルインストールしたパッケージのコマンドを実行するには以下の3つ(4つ目はグローバルインストールなのでカウント外)が挙げられます。

  • 方法1:パスを通す
    • $ ./node_modules/.bin/<package> のようにパスを通しながら実行する($(npm bin)/<package> でも同じ挙動)
  • 方法2:npm-srcipts
    • package.json の scripts の項目に { "scripts" : { "key" : "value" }}"value" にコマンドを指定したのちに $ npm run <key> と実行する
    • ※2 $ npm run <key> と実行するとローカルインストール先(./node_modules/.bin/)にパスを通しながらコマンドを実行してくれる
  • 方法3:npx
    • npx <package> とコマンドを実行すると、ローカルインストール先にパッケージがあればそれを使い、なければ一時的にローカル環境にパッケージをインストールして実行する(実行後は一時的にインストールしたパッケージは削除される)
  • (方法4:グローバルインストール)
    • $ npm install -g <package> 後に $ <package> とコマンド実行する

基本的に npm-srcipts または npx での実行をお勧めします。

ローカルインストールしたパッケージのコマンドを頻繁に実行するコマンドであったりプロジェクトで共有すべきコマンドであれば npm-srcipts に登録したほうがいいと思います。

一方で、たまにしか実行しないコマンドであったり、動作の検証であれば、わざわざ npm-srcipts に登録せずに npx での実行でいいと思います。
むしろ、むやみに npm-srcipts に登録すると管理する対象が増えるのでお勧めしません。

さらには npx はローカルインストールされていないパッケージは、一時的にパッケージをインストールして実行してくれるので、そもそも、たまにしか実行しないコマンドであったり、動作の検証でであれば、ローカルインストールすらしないで、npx 経由で毎回インストールしながらコマンド実行してもいいと思います。
時間はかかりますが、検証の結果不要になった場合にパッケージのアンインストールのし忘れなどがないです。

さらにさらに、よくネットに転がっている記事はやたらとグローバルインストールさせてきますが、そのとき安易にグローバルインストールしないほうがいいです。
大抵の場合 npx を使って一時的にインストールするだけで済むケースがほとんどです。( npx 最強!!)

(参考)

(npxでnodeモジュールを実行する)[https://qiita.com/tatakahashiap/items/1c4ab221c4993e7c4ebf]

使い分けをまとめると以下になります。

ケース インストール先 CLIでの実行方法
・よく実行する
・プロジェクトで共有して管理したい
ローカルインストール npm-srcipts
・まあまあ実行する
・今のところ自分だけ知っていればいい
・バージョンを管理したい
ローカルインストール npx
・たまにしか実行しない
・検証中である
・ちょっと時間がかかってもよい
・環境に影響をかけたくない
・とりあえず実行させたい
npx による一時的インストール npx
・それ以外
・どうしてもグローバルインストールしたい
グローバルインストール 普通に実行

方法1:パスを通す

# ローカルインストール先のパスを指定した cowsay コマンドの実行
$ ./node_modules/.bin/cowsay "hoge" 
 ______
< hoge >
 ------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

# $(npm bin) を使用しても同様にローカルインストール先のパスを指定したことになる(./node_modules/.bin/<package> = $(npm bin)/<package>)
$ $(npm bin)/cowsay "hoge"   
 ______
< hoge >
 ------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

方法2:npm-srcipts

# package.json の scripts に cowsay を追加(npm run scripts<key> とコマンドを実行するとローカルインストール先のパスを通しながら scripts<value> コマンドを実行する)
$ vi package.json
{
  (省略)
  "dependencies": {
    "cowsay": "^1.4.0"
  },
  "scripts": {
+   "cowsay": "cowsay"
  },
  (省略)
}

# npm-scripts での cowsay コマンドの実行
$ npm run cowsay -- "hoge" # npm-scripts の引数のオプションは -- のあとに指定する(-- なくても大丈夫な場合もあるがつけた方が無難)

> typescript-node-base@1.0.0 cowsay /Users/sugurutakahashi/git/typescript-node-base
> cowsay "hoge"

 ______
< hoge >
 ------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

# npm run 実行時に -s (= --silent) の オプションをつけると出力がすっきりします
$ npm run -s cowsay -- "hoge"
 ______
< hoge >
 ------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

【脱線】 npm-scripts の pre、 post プレフィックス機能

npm-scriptsには、pre または post プレフィックスをつけることで、その npm-scripts の前後に実行される処理を記述することができます。

  • pre
    • その npm-scripts の前に実行される
    • ex) prestartstart の前に実行される
  • post
    • その npm-scripts の後に実行される
    • ex) prestartstart の後に実行される
# cowsay 実行前に実行する precowsay、cowsay 実行後に実行する postcowsay を追加(内容はechoされる簡単なもの)
$ vi package.json
{
  (省略)
  "dependencies": {
    "cowsay": "^1.4.0"
  },
  "scripts": {
    "cowsay": "cowsay",
+   "precowsay": "echo 'pre cowsay'",
+   "postcowsay": "echo 'post cowsay'"
  },
  (省略)
}

# npm-scripts の cowsay コマンド実行によって package.json の npm-scripts の precowsay と postcowsay が実行されることの確認
$ npm run -s cowsay -- "hoge"
pre cowsay
 ______
< hoge >
 ------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
post cowsay

【脱線】 npm-scripts の 順次・並列実行(npm-run-all)

npm-run-all は複数の npm-scripts を実行できるコマンドラインツールです。
オプションをつけることで引数の npm-scripts の順次実行 または 並列実行することができます。

  • 順次実行
    • npm-run-all --serial <task> = npm-run-all -s <task> = run-s <task>
  • 並列実行
    • npm-run-all --parallel <task> = npm-run-all -p <task> = run-p <task>
# 以下のような hello:foo と hello:bar という npm-scripts を追加
$ vi package.json
{
  (省略)
  "scripts": {
+   "hello:foo": "sleep 1; echo FOO",
+   "hello:bar": "sleep 1; echo BAR"
  },
  (省略)
}

# 1秒後に "FOO" を echo する
$ npm run -s hello:foo
FOO

# 1秒後に "BAR" を echo する
$ npm run -s hello:bar
BAR

# npm-run-all のパッケージをインストール
$ npm install --save-dev npm-run-all

# hello:foo と hello:bar を順次実行する hello-s と hello:foo と hello:bar を並列実行する hello-p を登録
$ vi package.json
{
  (省略)
  "devDependencies": {
+   "npm-run-all": "^4.1.5"
  },
  "scripts": {
+   "hello-s": "npm-run-all -s hello:*",
+   "hello-p": "npm-run-all -p hello:*",
    "hello:foo": "sleep 1; echo FOO",
    "hello:bar": "sleep 1; echo BAR"
  },
  (省略)
}

# FOO が echo されて 1秒後に BAR が echo される(順次実行)
$ npm run -s hello-s
FOO
BAR

# FOO と BAR がほぼ同時に echo される(並列実行)
$ npm run -s hello-p
BAR
FOO

方法3:npx

# npx <package> コマンドでもローカルインストール先のパスを通しながら <package> のコマンドを実行できる(ローカルインストールしている場合は ./node_modules/.bin/<package> = npx <package>)
$ npx cowsay "hoge"
 ______
< hoge >
 ------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

# node_modules のない適当なディレクトリに移動
$ cd ../
$ npm list --depth=0
/Users/sugurutakahashi/git
└── (empty)

# npx <package> コマンド実行時にローカルインストール先(node_modules)にパッケージが存在しない場合は、一時的にローカルにパッケージをインストールして実行する(実行後は一時的にインストールしたパッケージは削除される)
$ npx cowsay "hoge"
npx: 10個のパッケージを1.636秒でインストールしました。
 ______
< hoge >
 ------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

npx でパッケージ名とコマンドが異なる場合(npx -p <package> -c "コマンド")(脱線)

例えば express-generator のパッケージでは使用するコマンドは express というように、パッケージ名とコマンドが異なる場合は npx -p <package> -c "コマンド" とします。

挙動をみると npx -p <package> -c "コマンド" と実行する場合は、ローカルインスルされたパッケージの有無に関わらず、リモートのパッケージをインストールするみたいです。

# express というコマンドは存在しないため失敗する
$ npx express --version          
npx: 50個のパッケージを1.814秒でインストールしました。
コマンドが見つかりません: express

# -p で express-generator パッケージ、-c で express コマンドを指定すると実行できる
$ npx -p express-generator -c "express --version"
npx: 10個のパッケージを1.277秒でインストールしました。
4.16.1

(方法4:グローバルインストール)

# もちろんグローバルインストールすればローカルインストールする必要もないし npm-scripts に登録する必要もないし、npx とつける必要もないが、その環境でしか実行できなくなる(非推奨)
$ cowsay "hoge"
zsh: command not found: cowsay
$ npm install -g cowsay
$ cowsay "hoge"
 ______
< hoge >
 ------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

基本的な npm install コマンド

グローバルインストール

# グローバルインストール
npm install -g <package>
npm install --global <package>

ローカルインストール

# ローカルインストール

# package.json の dependencies に追加するとき(npm v4 以下では --save (= -S) のオプションが必要、npm v5 以上ではデフォルトになったため不要)
npm install <package>
npm install --save <package> # デフォルトで入っているオプションなので使う必要なし
npm install -S <package> # デフォルトで入っているオプションなので使う必要なし

# package.json の devDependencies に追加するとき --save-dev (= -D)
npm install --save-dev <package>
npm install -D <package>

バージョン指定のインストール

# バージョンに関する指定
npm install <package>@x.y.z # バージョンを指定する場合
npm install <package>@latest # 最新版を指定する場合( @latest とつけなくても指定しなければ最新版がインストールされる)

package.json に記述されるバージョン情報について

dependencies または devDependencies に記述されているバージョン情報の チルダ ^ や キャレット ~ は以下の意味になります。

  • バージョン固定
    • ex) 3.2.1
    • npm install すると 3.2.1 のパッケージをインストールする
  • チルダ ~: マイナーバージョンまでの挙動を保証
    • ex) ~3.2.1
    • npm install すると 3.2.x の中の最新バージョンをインストールする
  • キャレット ^: メジャーバージョンまでの挙動を保証
    • ex) ^3.2.1
    • npm install すると 3.x.x の中の最新バージョンをインストールする
  • 指定なし: 全てのバージョンでの挙動を保証
    • ex) *
    • npm install すると x.x.x の中の最新バージョンをインストールする

npm のバージョン管理は セマンティック バージョニング に準拠しているはずのため、ほとんどがキャレット ^ 指定のバージョンが package.json に記述されます。

npm install <package>npm install -D <package> の違い

npm install <package> を行うと package.json の dependencies という項目にパッケージ名とそのバージョン情報が記述されます。
また -D--save-dev とオプジョンをつけて npm install -D <package> と実行すると devDependencies という項目にパッケージ名とそのバージョン情報が記述されます。

一般的に、開発環境でしか使用しないパッケージについては npm install -D <package> として devDependencies に記述し、そうではないパッケージについては npm install <package> として dependencies に記述します。

dependenciesdevDependencies のどちらに記述されるかによって、npm installnpm install --production の挙動が異なります。

オプションなしの npm install コマンドを実行をすると package.json の dependenciesdevDependencies の両方に記述されているパッケージをインストールします。

一方で --production のオプションをつけて npm install --production と実行すると dependencies に記述されているパッケージのみインストールし、devDependencies のパッケージに関してはインストールしません。

--production のオプションをつけることによって開発環境でしか使わないパッケージを除外できるので、本番環境デプロイ時に参照されることのないパッケージをインストールしなくて済むようになります。

実行するコマンド package.json の記述先 それぞれの使い分け
npm install <package> dependencies 開発環境以外でも使用されるパッケージ
ex) express
npm install -D <package> devDependencies 開発環境のみで使用されるパッケージ
ex) eslint
実行するコマンド インストール対象 実行するタイミング
npm install dependencies
devDependencies
開発環境
npm install --production dependencies のみ 本番環境など開発環境でしか使われないパッケージをインストールしたくないとき

正直、パッケージを dependenciesdevDependencies のどちらに入れるべきかはケースによって異なると思うので、実際に npm install --production をするときになってから考えればいいと思います。

【参考】

(【package.json】dependencies, devDependencies の使い分けを考える)[https://qiita.com/karur4n/items/3d9d28f6f21c3533020d]

npm install の実行時の package-lock.json の有無による挙動の整理

  • package-lock.json が存在しないとき

    • package.json に基づいてインストールされる
    • 実際にインストールしたバージョンの内容で package-lock.json が作成される
  • package-lock.json が存在するとき

    • package-lock.json に基づいてインストールされる
    • package.json で指定されたバージョンとの矛盾があれば package.json が優先される
    • 実際にインストールしたバージョンの内容で package-lock.json が更新される

つまり、package-lock.json は実際にインストールしたバージョンが常に記載されるということである。

package-lock.json を使う方法

npm ci というコマンドを実行すると package-lock.json から node_modules を作成してくれる。
「node_modules を作成する」という意味合い的には npm installnpm ci と一緒である。

# package.json のキャレットやチルダからできるだけ最新のバージョンで node_modules を作成するとき
npm install

# package-lock.json のバージョンで node_modules を作成するとき
npm ci

npm cinpm install の要点をまとめると以下のようなことがあげられる

  • npm cinpm install と同じように全依存パッケージをインストールする
  • npm installpackage-lock.json を更新することがある
  • npm ci は package-lock.json を更新しない
  • npm ci は node_modules を削除してからインストールする

npm ci の使い所

最も使用頻度の高いユースケースとしては CI 実行時があげられる。

理由としては、package-lock.json が勝手に更新されるのを防いだり、node_modules の更新によって動作が変わってしまうことを防ぐためである。
同様の理由から、新規参画者が git clone して動作を確認する場合なども npm ci が用いられる。

(※ ただし CI の速度を求めるなら npm ci は実行時間がやや長いため node_modules をキャッシュして npm install を用いることもある。)

よくある使い所
- CI 実行時
- git clone 直後の動作確認
- 何かのトラブルで node_modules を空にしてやり直すとき
- 過去の状態を復元するとき

結論

  • package-lock.json は npm install で実際にインストールした内バージョンの内容が記述されている
  • package-lock.json を参照して node_modules を作成するときは npm ci コマンドを実行する
  • 主なユースケースとしては CI 実行時、git clone 後の動作確認などで使用する

セマンティック バージョングについて (脱線)

アプリ(主にAPI)のバージョンの付け方に関するルールに セマンティック バージョング = Semver (Semantic Versioning) というガイドラインが存在する。

セマンティック バージョニング 2.0.0

概要

バージョンナンバーは、メジャー.マイナー.パッチ とし、バージョンを上げるには、

  1. APIの変更に互換性のない場合はメジャーバージョンを、
  2. 後方互換性があり機能性を追加した場合はマイナーバージョンを、
  3. 後方互換性を伴うバグ修正をした場合はパッチバージョンを上げます。  プレリリースやビルドナンバーなどのラベルに関しては、メジャー.マイナー.パッチ の形式を拡張する形で利用することができます。

出典:セマンティック バージョニング

npm install 実行後に作成される package.json や package-lock.json にバージョン情報が記載されるが、そのバージョン情報はこちらのセマンティック バージョニングのルールに準拠してバージョン管理されている。

npm-check-updates での package.json に記載されているパッケージのバージョンアップ

package.json に記載されているパッケージのバージョン情報をアップデートしたい場合は以下の手順を踏む必要があります。

  1. npm outdated で新しいバージョンがリリースされてないか確認する
  2. 新しいバージョンがリリースされていた場合、該当のパッケージを package.json から削除する(これがいっぱいあるとしんどい
  3. npm install で再度パッケージをインストールする

npm updatenpm update <package> というコマンドもありますが、package.json の内容から依存関係のバージョンの記載の範囲内の最新版をインストールして package-lock.json や node_modules を更新するだけで、package.json の dependenciesdevDependencies に記載されているバージョン情報は最新のものには更新されません。

(正直、グローバルインストールしたパッケージをバージョンアップする場合を除いて npm update が必要となるケースがあまりわかりません。基本的に npm install でこと足りるという認識です。)

npm-check-updates というパッケージを使用すればとても以下のコマンドだけの手順でアップデートが可能です。

  1. ncu コマンドの実行 (アップデート情報の確認)
  2. ncu -u コマンドの実行(package.json の更新)
  3. npm install コマンドの実行(更新された package.json をもとにパッケージをインストール)

実行例は以下の通りです。cowsay の古いパッケージを package.json ごとアップデートします。

# ncu でアップデート可能なパッケージの確認(ほとんどの記事ではグローバルインストールしていますが使用頻度は多くないと思うので npx での実行でもいいと思います)
$ npx -p npm-check-updates  -c "ncu"
npx: 285個のパッケージを6.971秒でインストールしました。
Checking package.json
[====================] 2/2 100%

 cowsay  ^1.2.0  →  ^1.4.0   

Run ncu -u to upgrade package.json


# ncu -u を実行すると package.json が更新される
$ npx -p npm-check-updates  -c "ncu -u"
npx: 285個のパッケージを6.971秒でインストールしました。
Checking package.json
[====================] 2/2 100%

 cowsay  ^1.2.0  →  ^1.4.0   

Run npm install to install new versions.


# 更新された package.json をもとに npm install の実行
$ npm install

(参考)
npm installしたパッケージの更新確認とアップデート(npm-check-updates)

gibo

gibo とは .gitignore を自動的に作るツールである。

同じようなツールとして gitignore.io というサービスがある。

メリット・デメリットそれぞれあるが、一度リポジトリを引っ張ってくればオフラインで完結できる gibo の方が優秀のように思える。
直感的には gitignore.io のほうがわかりやすい。

正直どちらでもよさそう。

(参考)
giboでgitignoreを自動生成する
.gitignoreを自動的に作る(gibo, gitignore.io を使う)

EditorConfig

どんな IDE・エディタ でもコーディンングスタイルを定義、維持するツール。
.editorconfig というファイルにルールを定義する。

正直、開発するときはチームで IDE・エディタを合わせるので、あんまり必要性は感じない(今のところ VSCode 一強)。

(参考)
どんなエディタでもEditorConfigを使ってコードの統一性を高める

package.json の config の使い方について(脱線)

package.json の config に項目を追加すると npm-scripts 実行時に環境変数として $npm_package_config_xxx ( xxx はプロパティ名) という形で使用できるようになります。
使い方は以下のような方法で使うことができます。

package.json
{
  "name": "foo",
  "config": {
    "foo": "bar",
    "dev": {
      "port": 8080
    }
  },
  "scripts": {
    "start": "node ./index.js",
    "dev": "http-server -p $npm_package_config_dev_port"
  }
}
index.js
console.log(process.env.npm_package_config_dev_port); // 8080
console.log(process.env.npm_package_config_foo_bar); // baz

正直、使い所はあまりないと思います。
ローカルの設定を package.json に書き込むことになってしまい、git で package.json が競合することになります。

npm-scripts はコマンドライン引数をコマンドに渡せるためコマンドライン引数を使ったほうがいいです。

使用しているプロジェクトもたまにあるのでとりあげました。

yarn について(脱線)

以下の意見にとても納得しました。

ご参考まで。

ちなみに: 似たような CLI として Facebook が開発した Yarn がある。これは npm の色々な欠点(スピードなど)を補うように作られたものであり、かなり人気がある。npm パッケージの README でしばしば npm と yarn でインストールする方法が両方書かれていたり、時には「yarn を使用することを推奨する」と書かれていたりする。しかし、npm も改善されてきており、わざわざ yarn をインストールして使用するメリットはあまりないと筆者は考えている。特に初心者にとっては、スタンダードでないツールを使用すると無駄に学ぶことが増えるのでおすすめしない。

【初心者向け】NPMとpackage.jsonを概念的に理解する (抜粋)
https://qiita.com/righteous/items/e5448cb2e7e11ab7d477

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初心者】そろそろ適当に npm install するのを卒業する!!

はじめに

ネットに転がっている記事などで npm install のコマンドをよく分からず実行してきましたが、そろそろその状態から卒業したかったので備忘録をかねてこちらの記事を投稿しました。

npm install コマンドに関することをメインにお伝えしますが、package.json や node_modules など npm を語る上で基本的なことについても触れていきたいと思います。

動作環境

# Node.js のバージョン確認
$ node -v
v14.13.1

# npm のバージョン確認
$ npm -v
6.14.8

ちなみに npm 自体のアップデートは以下のコマンドになります。
npm はメジャーバージョンが異なるとかなり挙動が異なるので、参照する記事の npm のバージョンにも注意が必要です。

# npm のアップデート
$ npm update -g npm

npm install を説明する前に知っておきたいこと

この辺を理解しておかないと適当に npm install してしまうことになるので、先に説明します。
知っている方は飛ばしても大丈夫です。

Node.js

Node.js とはサーバサイドで動く JavaScript のことです。

npm

npm(Node Package Manager)とは Node.js のパッケージ(Package)を管理する(Manager)ツールです。

Node.jsのパッケージ(Package)とは予め用意された便利な機能(各種フレームワークやライブラリ)をまとめたものです。

yarn

yarn とは2016年にリリースされた npm と互換性のあるパッケージマネージャーです。

今回は yarn コマンドについては触れませんが、以下の意見にとても納得しました。
ご参考まで。

ちなみに: 似たような CLI として Facebook が開発した Yarn がある。これは npm の色々な欠点(スピードなど)を補うように作られたものであり、かなり人気がある。npm パッケージの README でしばしば npm と yarn でインストールする方法が両方書かれていたり、時には「yarn を使用することを推奨する」と書かれていたりする。しかし、npm も改善されてきており、わざわざ yarn をインストールして使用するメリットはあまりないと筆者は考えている。特に初心者にとっては、スタンダードでないツールを使用すると無駄に学ぶことが増えるのでおすすめしない。

【初心者向け】NPMとpackage.jsonを概念的に理解する (抜粋)
https://qiita.com/righteous/items/e5448cb2e7e11ab7d477

package.json

package.json とは Node.js ベースの JavaScript アプリ開発において、自身のパッケージ(= プロジェクトそのもの)を管理するために使われるファイルのことです。

npm init とコマンド実行すると package.json が作成されます。

package-lock.json

簡単に説明すると package.json を用いてパッケージをインストールした結果が記載されるファイルです。
詳しくは【参考】のリンク先を参照してください。

【参考】
package-lock.json ってなに?

npm init

package.json は普通に touch コマンドなどでも作成できますが、基本的には npm init とコマンド実行して package.json を作成します。

(【脱線】npm init しないとどうなる?

# 何もない空のディレクトリ
$ ls
(標準出力なし)

# npm init による package.json の作成(質問をされるが一旦すべて Enter で通過する = npm init -y コマンド実行時と同じ挙動)
$ npm init

# package.json が作成されている
$ ls
package.json

package.json の初期値は以下のようなものになります。

package.json
{
  "name": "<current directory name>",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

package.json にはいろいろ項目がありますが name, version, description, keywords, author, license などのデータは単なる自身のパッケージ(= プロジェクトそのもの)のメタデータであり、自身のパッケージ(= プロジェクトそのもの)を公開しなのであれば、気にする必要はありません。

dependencies, devDependencies, scripts, config の4つぐらいを抑えれば、あとは必要に応じて調べて設定すれば問題ないかと思います。

【参考】
package.jsonの構造

npm と package.json の関係

公開されているパッケージ(ライブラリやフレームワークなど)は npm コマンドによってインストールすることができます。

package.json が存在するディレクトリで npm コマンドによってパッケージをインストールすると、自動的に package.json が更新されます。

また、人間が package.json を直接編集することはありません。

すべて npm コマンド経由で更新します。(大事)

node_modules

node_modules とは package.json を元にしてインストールされる各種パッケージのがインストールされるディレクトリ先のことです。

package.json さえあれば、npm install コマンドの実行によって node_modules が生成することができるため、通常 .gitignore に指定されるディレクトリです。

node_modules はいろんなパッケージがインストールされているため、とても容量の大きなディレクトリになるので、その管理を package.json や package-lock.json にまかせて node_modules 自体の管理は git で管理対象外にするケースがほとんどです。

npm install

ここからがこの記事の本題になります。

パッケージを node_modules にインストールするには npm install とコマンド実行します。

この npm install というコマンドですが、これがいろいろと種類があってややこしいので詳しく説明していきます。

とりあえずよく使うコマンド一覧

引数なしでの実行する場合

# 引数なし実行(package.json のあるディレクトリで実行する)
npm install

グローバルインストール

# グローバルインストール
npm install -g <package>
npm install --global <package>

ローカルインストール

# ローカルインストール
# package.json の dependencies に追加するとき(npm v4 以下では --save (= -S) のオプションが必要、npm v5 以上ではデフォルトになったため不要)
npm install <package>
npm install --save <package> # デフォルトで入っているオプションなので使う必要なし
npm install -S <package> # デフォルトで入っているオプションなので使う必要なし

# package.json の devDependencies に追加するとき --save-dev (= -D)
npm install --save-dev <package>
npm install -D <package>

バージョン指定のインストール

# バージョンに関する指定
npm install <package>@x.y.z # バージョンを指定する場合
npm install <package>@latest # 最新版を指定する場合( @latest とつけなくても指定しなければ最新版がインストールされる)

引数のない npm install の挙動

引数のない npm install コマンド実行時の挙動は、引数のある npm install <package> とわけて考えたほうがわかりやすいです。

引数のない npm install コマンド実行をすると、カレントディレクトリにある package.json に記述されている情報を元に、そこに記述されている パッケージを node_modules (インストール先)にインストールします。

なので、あらかじめ package.json にインストールしたいパッケージ情報を記述しておく必要があります。

そこで実行するのが引数のある npm install <package> になります。

引数のある npm install <package> の種類

大きな分類としてグローバルインストールとローカルインストールに分けられます。
また、ローカルインストールのなかでも package.json の記述先の違いで2つに分けられます。

全体で見るとグローバルインストール1種類、ローカルインストール2種類の合計3種類になります。

大分類 package.json の記述先 コマンド
グローバルインストール - npm install -g <package>
ローカルインストール dependencies npm install <package>
ローカルインストール devDependencies npm install -D <package>

ちなみに「グローバルインストール」と「ローカルインストール」の両方とも自身のPCの環境へのインストールのことです。

グローバルインストール

グローバルインストールすると自身の PC の環境ならどこでもインストールしたパッケージ(コマンド)が実行できます。

ただし、グローバルインストールしても nodebrew などで複数の Node.js のバージョンを管理している場合は、それぞれのバージョンで npm install -g <package> したパッケージが、それぞれ別のものとして管理されるので注意が必要です。

【参考】
【npm】 パッケージのインストール先の確認(npm list)

グローバルインストールの挙動

# 未インストールなので cowsay コマンドは実行できない
$ cowsay "hoge"
zsh: command not found: cowsay

# cowsay コマンドをグローバルインストール
$ npm install -g cowsay
$ cowsay "hoge"
 ______
< hoge >
 ------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

ローカルインストール

ローカルインストールすると node_modules と同じディレクトリにある場合にパッケージ(コマンド)が実行できます。

【参考】
npm でローカルインストールしたパッケージを CLI でコマンド実行する方法(npm-srcipts, npx)

ローカルインストールの挙動( -D オプションなしの場合)

# 適当なパッケージ(cowsay)をインストール
$ npm install cowsay
+ cowsay@1.4.0

# dependencies の項目に cowsay が追加されていることが確認できる
$ cat package.json
{
  (省略)
+ "dependencies": {
+   "cowsay": "^1.4.0"
+ },
  (省略)
}

# node_modules にインストールされるのでパスを通しながら実行可能
$ ./node_modules/.bin/cowsay "hoge" 
 ______
< hoge >
 ------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

ローカルインストールの挙動( -D オプションありの場合)

# 適当なパッケージ(typescript)を -D のオプションをつけてインストール
$ npm install -D typescript
+ typescript@4.0.3

# devDependencies の項目の typescript が追加されていることが確認できる
$ cat package.json
{
  (省略)
  "dependencies": {
    "cowsay": "^1.4.0"
  },
+ "devDependencies": {
+   "typescript": "^4.0.3"
+ },
  (省略)
}

# -D のオプションをつけても変わらずに node_modules にインストールされるのでパスを通しながら実行可能
$ ./node_modules/.bin/tsc --version 
Version 4.0.3

グローバルインストール と ローカルインストール使い分け

では、パッケージをインストールする場合、グローバルインストールローカルインストール のどちらにインストールすべきでしょうか?

状況によりますが、闇雲にグローバルインストールするのは避けたほうがいいです。

グローバルインストールされたパッケージは環境が異なると使用することができません。

例えば、PC を買い替えたらそのパッケージを使用することができないですし、他人の PC でもそのパッケージを使用することができません。

そいういうときにローカルインストールで作成される package.json だけを共有すれば、どの環境でも npm install を実行するだけで、同じパッケージを使用することができます。

なので、ローカルインストールをお勧めします。

ローカルインストール時に -D オプションをつけるべきケース

一般的に、開発環境でしか使用しないパッケージについては npm install -D <package> として、そうではないパッケージについては npm install <package> とします。

パッケージ名の引数をとらない npm install には --production とオプションをつけることで、npm install -D <package> でインストールしたパッケージを除いて node_modules にインストールします。

--production のオプションをつけることによって開発環境でしか使わないパッケージを除外できるので、本番環境デプロイ時に参照されることのないパッケージをインストールしなくて済むようになります。

ローカルインストール時の -D オプションの有無による違いをまとめると以下のようになります。

実行するコマンド package.json の記述先 それぞれの使い分け
npm install <package> dependencies 開発環境以外でも使用されるパッケージ
ex) express
npm install -D <package> devDependencies 開発環境のみで使用されるパッケージ
ex) eslint

また、パッケージ名の引数をとらない npm install 実行時の --production オプションの有無による違いをまとめると以下のようになります。

実行するコマンド インストール対象 実行するタイミング
npm install dependencies
devDependencies
開発環境
npm install --production dependencies のみ 本番環境など開発環境でしか使われないパッケージをインストールしたくないとき

いろいろ書いていますが、ケースバイケースなので、実際に npm install --production をするときになってから考えればいいと思います。

【参考】
【package.json】dependencies, devDependencies の使い分けを考える

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初心者】そろそろ適当に npm install するのを卒業したい!!

はじめに

ネットに転がっている記事などで npm install のコマンドをよく分からず実行してきましたが、そろそろその状態から卒業したかったので備忘録をかねてこちらの記事を投稿しました。

npm install コマンドに関することをメインにお伝えしますが、package.json や node_modules など npm を語る上で基本的なことについても触れていきたいと思います。

この記事の目標

この記事の目的は、以下の4つの npm install コマンドを実行したときの挙動と、これら4つのコマンドを必要な状況に応じて使いわけるようになることです。

  • npm install
  • npm install -g <package>
  • npm install <package>
  • npm install -D <package>

つまり、適当に npm install するのを卒業することです。

動作環境

# Node.js のバージョン確認
$ node -v
v14.13.1

# npm のバージョン確認
$ npm -v
6.14.8

ちなみに npm 自体のアップデートは以下のコマンドになります。
npm はメジャーバージョンが異なるとかなり挙動が異なるので、参照する記事の npm のバージョンにも注意が必要です。

# npm のアップデート
$ npm update -g npm

npm install を説明する前に知っておきたいこと

この辺を理解しておかないと適当に npm install してしまうことになるので、先に説明します。
知っている方は飛ばしても大丈夫です。

Node.js

Node.js とはサーバサイドで動く JavaScript のことです。

npm

npm(Node Package Manager)とは Node.js のパッケージ(Package)を管理する(Manager)ツールです。

Node.jsのパッケージ(Package)とは予め用意された便利な機能(各種フレームワークやライブラリ)をまとめたものです。

yarn

yarn とは2016年にリリースされた npm と互換性のあるパッケージマネージャーです。

今回は yarn コマンドについては触れませんが、以下の意見にとても納得しました。
ご参考まで。

ちなみに: 似たような CLI として Facebook が開発した Yarn がある。これは npm の色々な欠点(スピードなど)を補うように作られたものであり、かなり人気がある。npm パッケージの README でしばしば npm と yarn でインストールする方法が両方書かれていたり、時には「yarn を使用することを推奨する」と書かれていたりする。しかし、npm も改善されてきており、わざわざ yarn をインストールして使用するメリットはあまりないと筆者は考えている。特に初心者にとっては、スタンダードでないツールを使用すると無駄に学ぶことが増えるのでおすすめしない。

【初心者向け】NPMとpackage.jsonを概念的に理解する (抜粋)
https://qiita.com/righteous/items/e5448cb2e7e11ab7d477

package.json

package.json とは Node.js ベースの JavaScript アプリ開発において、自身のパッケージ(= プロジェクトそのもの)を管理するために使われるファイルのことです。

npm init とコマンド実行すると package.json が作成されます。

package-lock.json

簡単に説明すると package.json を用いてパッケージをインストールした結果が記載されるファイルです。
詳しくは【参考】のリンク先を参照してください。

【参考】
package-lock.json ってなに?

npm init

package.json は普通に touch コマンドなどでも作成できますが、基本的には npm init とコマンド実行して package.json を作成します。

(【脱線】npm init しないとどうなる?

# 何もない空のディレクトリ
$ ls
(標準出力なし)

# npm init による package.json の作成(質問をされるが一旦すべて Enter で通過する = npm init -y コマンド実行時と同じ挙動)
$ npm init

# package.json が作成されている
$ ls
package.json

package.json の初期値は以下のようなものになります。

package.json
{
  "name": "<current directory name>",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

package.json にはいろいろ項目がありますが name, version, description, keywords, author, license などのデータは単なる自身のパッケージ(= プロジェクトそのもの)のメタデータであり、自身のパッケージ(= プロジェクトそのもの)を公開しなのであれば、気にする必要はありません。

dependencies, devDependencies, scripts, config の4つぐらいを抑えれば、あとは必要に応じて調べて設定すれば問題ないかと思います。

【参考】
package.jsonの構造

npm と package.json の関係

公開されているパッケージ(ライブラリやフレームワークなど)は npm コマンドによってインストールすることができます。

package.json が存在するディレクトリで npm コマンドによってパッケージをインストールすると、自動的に package.json が更新されます。

また、人間が package.json を直接編集することはありません。

すべて npm コマンド経由で更新します。(大事)

node_modules

node_modules とは package.json を元にしてインストールされる各種パッケージのがインストールされるディレクトリ先のことです。

package.json さえあれば、npm install コマンドの実行によって node_modules が生成することができるため、通常 .gitignore に指定されるディレクトリです。

node_modules はいろんなパッケージがインストールされているため、とても容量の大きなディレクトリになるので、その管理を package.json や package-lock.json にまかせて node_modules 自体の管理は git で管理対象外にするケースがほとんどです。

npm install

ここからがこの記事の本題になります。

パッケージを node_modules にインストールするには npm install とコマンド実行します。

この npm install というコマンドですが、これがいろいろと種類があってややこしいので詳しく説明していきます。

とりあえずよく使うコマンド一覧

引数なしでの実行する場合

# 引数なし実行(package.json のあるディレクトリで実行する)
npm install

グローバルインストール

# グローバルインストール
npm install -g <package>
npm install --global <package>

ローカルインストール

# ローカルインストール
# package.json の dependencies に追加するとき(npm v4 以下では --save (= -S) のオプションが必要、npm v5 以上ではデフォルトになったため不要)
npm install <package>
npm install --save <package> # デフォルトで入っているオプションなので使う必要なし
npm install -S <package> # デフォルトで入っているオプションなので使う必要なし

# package.json の devDependencies に追加するとき --save-dev (= -D)
npm install --save-dev <package>
npm install -D <package>

バージョン指定のインストール

# バージョンに関する指定
npm install <package>@x.y.z # バージョンを指定する場合
npm install <package>@latest # 最新版を指定する場合( @latest とつけなくても指定しなければ最新版がインストールされる)

引数のない npm install の挙動

引数のない npm install コマンド実行時の挙動は、引数のある npm install <package> とわけて考えたほうがわかりやすいです。

引数のない npm install コマンド実行をすると、カレントディレクトリにある package.json に記述されている情報を元に、そこに記述されている パッケージを node_modules (インストール先)にインストールします。

なので、あらかじめ package.json にインストールしたいパッケージ情報を記述しておく必要があります。

そこで実行するのが引数のある npm install <package> になります。

引数のある npm install <package> の種類

大きな分類としてグローバルインストールとローカルインストールに分けられます。
また、ローカルインストールのなかでも package.json の記述先の違いで2つに分けられます。

全体で見るとグローバルインストール1種類、ローカルインストール2種類の合計3種類になります。

大分類 package.json の記述先 コマンド
グローバルインストール - npm install -g <package>
ローカルインストール dependencies npm install <package>
ローカルインストール devDependencies npm install -D <package>

ちなみに「グローバルインストール」と「ローカルインストール」の両方とも自身のPCの環境へのインストールのことです。

グローバルインストール

グローバルインストールすると自身の PC の環境ならどこでもインストールしたパッケージ(コマンド)が実行できます。

ただし、グローバルインストールしても nodebrew などで複数の Node.js のバージョンを管理している場合は、それぞれのバージョンで npm install -g <package> したパッケージが、それぞれ別のものとして管理されるので注意が必要です。

【参考】
【npm】 パッケージのインストール先の確認(npm list)

グローバルインストールの挙動

# 未インストールなので cowsay コマンドは実行できない
$ cowsay "hoge"
zsh: command not found: cowsay

# cowsay コマンドをグローバルインストール
$ npm install -g cowsay
$ cowsay "hoge"
 ______
< hoge >
 ------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

ローカルインストール

ローカルインストールすると node_modules と同じディレクトリにある場合にパッケージ(コマンド)が実行できます。

【参考】
npm でローカルインストールしたパッケージを CLI でコマンド実行する方法(npm-srcipts, npx)

ローカルインストールの挙動( -D オプションなしの場合)

# 適当なパッケージ(cowsay)をインストール
$ npm install cowsay
+ cowsay@1.4.0

# dependencies の項目に cowsay が追加されていることが確認できる
$ cat package.json
{
  (省略)
+ "dependencies": {
+   "cowsay": "^1.4.0"
+ },
  (省略)
}

# node_modules にインストールされるのでパスを通しながら実行可能
$ ./node_modules/.bin/cowsay "hoge" 
 ______
< hoge >
 ------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

ローカルインストールの挙動( -D オプションありの場合)

# 適当なパッケージ(typescript)を -D のオプションをつけてインストール
$ npm install -D typescript
+ typescript@4.0.3

# devDependencies の項目の typescript が追加されていることが確認できる
$ cat package.json
{
  (省略)
  "dependencies": {
    "cowsay": "^1.4.0"
  },
+ "devDependencies": {
+   "typescript": "^4.0.3"
+ },
  (省略)
}

# -D のオプションをつけても変わらずに node_modules にインストールされるのでパスを通しながら実行可能
$ ./node_modules/.bin/tsc --version 
Version 4.0.3

グローバルインストール と ローカルインストール使い分け

では、パッケージをインストールする場合、グローバルインストールローカルインストール のどちらにインストールすべきでしょうか?

状況によりますが、闇雲にグローバルインストールするのは避けたほうがいいです。

グローバルインストールされたパッケージは環境が異なると使用することができません。

例えば、PC を買い替えたらそのパッケージを使用することができないですし、他人の PC でもそのパッケージを使用することができません。

そいういうときにローカルインストールで作成される package.json だけを共有すれば、どの環境でも npm install を実行するだけで、同じパッケージを使用することができます。

なので、ローカルインストールをお勧めします。

ローカルインストール時に -D オプションをつけるべきケース

一般的に、開発環境でしか使用しないパッケージについては npm install -D <package> として、そうではないパッケージについては npm install <package> とします。

パッケージ名の引数をとらない npm install には --production とオプションをつけることで、npm install -D <package> でインストールしたパッケージを除いて node_modules にインストールします。

--production のオプションをつけることによって開発環境でしか使わないパッケージを除外できるので、本番環境デプロイ時に参照されることのないパッケージをインストールしなくて済むようになります。

ローカルインストール時の -D オプションの有無による違いをまとめると以下のようになります。

実行するコマンド package.json の記述先 それぞれの使い分け
npm install <package> dependencies 開発環境以外でも使用されるパッケージ
ex) express
npm install -D <package> devDependencies 開発環境のみで使用されるパッケージ
ex) eslint

また、パッケージ名の引数をとらない npm install 実行時の --production オプションの有無による違いをまとめると以下のようになります。

実行するコマンド インストール対象 実行するタイミング
npm install dependencies
devDependencies
開発環境
npm install --production dependencies のみ 本番環境など開発環境でしか使われないパッケージをインストールしたくないとき

いろいろ書いていますが、ケースバイケースなので、実際に npm install --production をするときになってから考えればいいと思います。

【参考】
【package.json】dependencies, devDependencies の使い分けを考える

さいごに

いかがだったでしょうか?
もうこれで、以下の4つの npm install コマンドはいけるはずです!!

  • npm install
  • npm install -g <package>
  • npm install <package>
  • npm install -D <package>

卒業おめでとうございます!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む