20200810のNode.jsに関する記事は9件です。

現場に残る Vuex の map ヘルパーのコードベースを消し去り、 Vue 3 時代の型に追従するための CLI ツールを TypeScript Compiler API で実現する

はじめに

Vue.js 製アプリケーションのレガシーコードベースにおいて、頻繁に課題となるのが mapGetters のような map ヘルパーのメソッドです。
これらはショートハンド的に使えて過去には便利なケースもありましたが、現在ではほとんど利用されることもなくなりました。

それもそのはず。現在のフロントエンド開発の主流となる言語は JavaScript ではなく TypeScript となっています。しかし、 map 系ヘルパーはその構造から任意の文字列を受け取った上でオブジェクトに影響を及ぼす形となっており、根本的に型システムとの相性が悪い存在です。
これを利用している限り、 Vue Component において map ヘルパーから this に生えたものは、型もつかなければそもそも this に生えていることすら TypeScript 側で検知できず、コンパイルエラーとなってしまいます。

そのため、今では Vue.js + TypeScript でのプロジェクトでは利用されることがほとんどなくなった map ヘルパーですが、記述時点で TypeScript が導入されていなかったコードベースでは、利便性からこれらのヘルパー関数が利用されているコードが残っていることもしばしばあります。

すぐに置き換えられると理想ですが、ネームスペースのあるなしなどの都合で一括置換で終わりといかないのがなかなかつらいところ。

今回はそんな課題を解決するため、7月のオリンピック連休を生かして vuex-map-purge という CLI ツールを作ってみました。

この記事では、簡単にそのモチベーションと利用方法、内部の構造をご紹介します。

vuex-map-purge について

https://github.com/potato4d/vuex-map-purge

vuex-map-purge は、その名の通り map ヘルパー、 mapGettersmapMutations, そして mapActions を分解し、 this にそれ相当の methods または computed を定義してくれる CLI ツールとなります。

例によって例のごとく MIT ライセンスの OSS です。よかったら star とかつけてもらえると。

具体的には、例えばこのような Vuex が利用されている JavaScript あるいは TypeScript のコードベースがあったとき

example.vue
<template>
  <div></div>
</template>

<script>
import Vue from 'vue'
import { mapActions } from 'vuex'

export default Vue.extend({
  methods: {
    ...mapActions(['loginUser']),
    ...mapActions('ui', ['switchToEditorView'])
  }
})
</script>

以下のような、 map ヘルパーを削除したコードベースへと変換してくれます。TypeScript の場合は unknown で型が定義され、 JavaScript のコードベースの場合は型定義をスキップします。

example.vue
<template>
  <div></div>
</template>

<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
  methods: {
    loginUser(payload?: unknown) {
      return this.$store.dispatch('loginUser', payload)
    },
    switchToEditorView(payload?: unknown) {
      return this.$store.dispatch('ui/switchToEditorView', payload)
    },
  },
})
</script>

導入と利用

基本的にプロジェクトローカルではなく、手元の Node.js 環境にグローバルに導入して実行します。

$ npm i -g vuex-map-purge

執筆時点 (v0.1.2) では特に CLI オプションはなく、対象となるディレクトリを glob 形式の文字列として渡すことで実行できます。
アプリケーション内で glob での走査を行うため、 Prettier などと同様の感覚で引数を渡してください。

$ vuex-map-purge './src/**/*.vue'

vuex-map-purge は purge だけ行いますが、標準出力に影響のあったファイルを出力するため、実際の利用時は xargs などとの併用をオススメします。

$ vuex-map-purge './src/**/*.vue' | xargs prettier --write

このように実行することで、完全な purge が可能です。

なぜこれを利用するのか

TypeScript との親和性はもちろんですが、 来たる Vue 3 への準備 が大きなモチベーションです。
これは README にも記述されています。

Vuex 4.0 fixes a problem that Generics had with the Store in the previous Vuex, making it possible to build a more type-safe system.

However, Vuex's mapXXX utility, which exists in Vuex, does not solve the type problem and hinders future type-safe coding.

As a result, we needed a tool to eliminate mapXXX from existing Vue.js projects as soon as possible.

Vue 3 時代に利用可能となる Vuex 4.0 では、 Vuex が 3.x 時代まで抱えていた Vuex.Store<T>Tany でハードコーディングされている問題が改善されています。

これによって、 this.$store からアクセスするストア構造にユーザー側で型を付与することが可能 となります。
これは大きな Vuex + TypeScript の改善であり、自分たちで Vuex をラップしたような層を用意する必要がなくなります。

ですが前述の通り、 map ヘルパーは文字列とオブジェクトの複雑なマッピングにより実現しており、これ自体の型定義は改善されないように見えます。

そのため、現時点では Vue 3 時代にストアの型を完全に守るためには、 map ヘルパーを取り除く必要がある という状態です。

これまではどのみち Vuex.Store<any> のために移行の大きなモチベーションが沸かない人もいたかと思いますが、これからはやらない意味がなくなるため、需要も出てくるかなと思って開発しました。

しくみについて

今回、この purge のために TypeScript Compiler API を利用してみました。

これは TypeScript のパッケージに含まれるコンパイラの挙動に介入するための API であり、ざっくりいうと今回は以下のようなことをしています。

  • AST ベースで mapXXX を検知し、中の構造をチェック
    • その中で、 AST の種別によって名前空間付きの定義か、ルートの名前空間であるかなどをチェック
  • 上記でチェックした内容をもとにコードを生成し、 this 内にフィールドとして定義を追加
    • AST 上で正しいコードであることが担保された形で this へとメソッドなどを気軽に生やすことができる
  • 結果をコードテキストとして出力する

今回 Compiler API を利用したのは、以前 ESLint の独自ルールを制定しているときに AST を JavaScript で触るのが辛かったため、 型に強い AST 関連のツールキットがほしい というモチベーションでした。

TS なしで AST 触るのって鬼のように console.log してテストコードにしていく以外無理ゲーな気がしてるんですが、何か良いやり方あるんですかね……

実際に行っているステップは以下です。

  1. glob パッケージで glob を判定し、対象となるファイルを洗い出す
  2. cheerio で <script> ブロックを抜き出す
  3. Compiler API に対して自作した transformer (自作 TS プラグインみたいなもの) を渡して変換を実行
  4. Compiler API が吐き出したコードを <script> ブロックの中身に設定
  5. File I/O で書き出す
  6. 書き出したファイルのパスを標準出力に書き出す

本来は Vue の SFC パーサーを正しいものを利用するべきですが、パース自体はできてもパースしたものを再度書き直す処理ができるパーサーが見つからなかったので今回はこのスタイルです。

ちょっとしたリファクタリング程度なら正規表現で行うことも多いと思いますが、TypeScript Compiler API で AST を操作する場合は、基本的には想定するコード以外はスキップした後に、該当するコードだけに処理を行うことができるため、考慮漏れが起きづらいことや、テストコードとの親和性が非常に高いのが良い点かなと思いました。

AST に少しなれるとリファクタリングの効率化が進みそうなので、よかったらコードなど参考にしてもらえればと思います。

https://github.com/potato4d/vuex-map-purge/blob/master/transformers/purgeMapActions.ts

未実装の feature について

そんなわけで publish したばかりの vuex-map-purge ですが、現時点では対応できていない仕組みがいくつか存在するため、注意が必要です。

  • <script> を含むコードベースをうまく変換できない
    • SFC パーサーを導入していないことが原因であるため、近日中に対応します。
  • mapXXX の Object 記法の対応
    • 私が見た中ではこれの利用ケースがほぼ無いため実装から省いています
    • 今後実装予定自体はありますが、私自身が目にすることがないケースのため、モチベーションのある方は PR いただけると幸いです
  • store 内の型定義の反映
    • これは Nuxt.js に限定するなどの場合はストアの構造が割れているため簡単ですが、プロジェクトによってディレクトリ構成が不明なため省いています
    • 今後オプションで型定義を渡すなどで解決される可能性はあります
  • mapState の対応
    • mapState を利用することをやめましょう

上記以外にもなにか要望などあれば、Issue にお願いいたします。

https://github.com/potato4d/vuex-map-purge/issues

おわりに

今回はコードベースを楽に改善したいモチベーションが半分、型定義が十分な AST を触るツールとしての TypeScript Compiler API を利用してみたかったというのが半分でのツール作成となりました。

Vue 3 と合わせて利用可能となる Vuex 4 では、 Vuex.Store<T>any ハードコーディングが修正され、 store の型定義を正しく引き回すことができるようになります。

Vue 3 のコードベースにおいてどの程度 Vuex が利用されるかは未知数ですが、 依然として Vue 2.x からのマイグレーションでは、切っても切り離せない重要な役割になるのではないでしょうか。

そんな中、 TypeScript と親和性の低い map ヘルパーは常に課題として残り続けます。

早期に課題を解決するためにも、 vuex-map-purge が役に立てば幸いです。

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

dyld: Library not loaded: /usr/local/opt/icu4c/lib/libicui18n.64.dylibエラーの対処法

npmを久々に実行しようとしたら、以下のエラーで使えなくなっていましたので、使えるようにする備忘録です。

$ npm -g install パッケージ名
dyld: Library not loaded: /usr/local/opt/icu4c/lib/libicui18n.64.dylib
  Referenced from: /usr/local/bin/node
  Reason: image not found
zsh: abort      npm -g install パッケージ名

nodeを再インストール

$ brew reinstall node

僕はHomebrewでnodeをインストールしていましたので、reinstallでnodeを再インストールすると解決しました。

環境

  • macOS Catalina 10.15.5
  • Homebrew 2.4.9
  • Node.js 14.7.0

参考

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

Visual Studio Code + WSL2でGo開発環境構築

WindowsからTeraTerm等のターミナルソフトを使って、Linuxサーバにssh接続し、Linux上で開発をすることは、一般的に行っていると思う。しかし、Windows用のVS CodeのIntellisenseやdebug機能を使いたい!と思うこともよくある。そんなことが、WSL2を使うことで簡単にできちゃうので、紹介してみる。

インストール

Remote Development Extention

VS Codeの画面左側のExtensions(Ctrl + Shift + X)メニューで、Remote Developmentを検索し、インストール。とっても簡単。

image.png

WSL2 + Linux Distribution

ググればわかりやすいページがたくさんヒットするので、頑張りましょう。なお、私も書いてたりします。
Windows 10でLinuxを使う(WSL2)

Go tools

WSL2上のLinuxにGo Toolsをインストールする。Distributionで用意しているパッケージでもいいかもしれないが、私は何となく最新版をインストールした。Getting Started - The Go Programming Languageを見れば、簡単にインストールできる。
インストール後、$HOME/.profileに以下のPATH設定をお忘れなく。

GOPATH="$HOME/go"
PATH="$PATH:/usr/local/go/bin:$GOPATH/bin"

Go on Linux on WSL2

モジュール初期化

Linux上で、以下のコマンドを実行し、モジュールの初期化を行う。

mkdir hello
cd hello
go mod init hello

VS Code起動

上記で作成したhelloディレクトリで、以下のコマンドを実行。Windows上でVS Codeがhelloディレクトリをオープンした状態で起動する。

code .

VS Codeの画面左下にWSL: XXXXXと表示されていれば成功。

image.png

Go Extentionのインストール

VS Codeで、hello.goを作成すると、Go Extentionのインストールを勧められるので、Installをクリック。

image.png

インストール完了後、Reload Requiredをクリック。

image.png

この後、ソースコードの編集、実行時にUpdateInstallの問い合わせが何回かあるが、基本的に言われるがままに実行する。

Go build

ソースの編集後、Ctrl + F5で、コマンドパレットが開くのでGoを選択。

hello.go
package main

import (
    "fmt"
)

func main() {
    msg := "Hello, Wold!"
    fmt.Println(msg)
}

image.png

下記のように、DEBUG CONSOLEHello, World!と表示されればOK。

image.png

Ctrl + F5を実行するたびに、Goを選択するのは面倒なので、画面左のRun(Ctrl + Shift +D)メニューをクリックし、create a launch.json fileをクリックし、コマンドパレットからGoを選択。

image.png

launch.jsonファイルが開くが、編集は不要なので、そのまま閉じる。これで、Ctrl + F5実行時の問い合わせがなくなる。

Debug

F5キーによりデバッグもできる。ソースコードの行番号の左側をクリックすることでブレークポイントを設定。F5キーを実行すると、デバッグ開始しブレークポイントで処理が止まる。あとは、Continue(F5)Step Over(F10)Step Into(F11)等で遊びましょう。

image.png

リンク

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

WiiリモコンとヌンチャクとバランスボードをMQTTするぞ(1/2)

前回の投稿で、WiiリモコンをNode.jsから触れるようにしました。( WiiリモコンをNode.jsから操ってみよう )

今度は、WiiリモコンをMQTTに接続して、ブラウザから操ってみます。さらに、ヌンチャクやバランスボードにも対応させました。
以下の2回に分けて説明していこうと思います。

1.ヌンチャクとバランスボードにも対応させ、WiiリモコンをMQTTに接続する(今回はこちら)
2.ブラウザからWiiリモコンたちに接続する

image.png

完成するとブラウザからこんな感じで見えますが、それは次回の投稿で。先に、Webページのリンクを張っておきます。
 https://poruruba.github.io/WiiRemocon/html/

image.png

ソースコードを以下にアップしておきました。

poruruba/WiiRemocon
 https://github.com/poruruba/WiiRemocon

続きの投稿はこちら
 WiiリモコンとヌンチャクとバランスボードをMQTTするぞ(2/2)

Wiiリモコンからの受信イベント

Wiiリモコンからは、ボタン押下や加速度の変化など、状態が変わるたびに、Bluetoothでイベントが受信されます。その内容は、レポーティングモードによって変わり、設定することで、受信する内容を変えることができます。
ボタン押下イベントだけのレポーティングモードに設定した場合は、Wiiリモコンのボタンを押下したりはなしたりしたときのみイベントが通知されますが、加速度も返るようにレポーティングモードに設定すると、ひっきりなしにイベントが受信されるようになります。

イベントの内容は、以下に記載があります。以降ではデータレポーティングと呼ぶことにします。
http://wiibrew.org/wiki/Wiimote#Data_Reporting

様々な内容が送られてきますので、上記の記載内容に従ってパースする関数を用意しました。parseReporting(data) です。

wiiremocon.js
・・・
  parseReporting(data){
    if( data[0] == WIIREMOTE_REPORTID_STATUS ){
      var report = {
        report_id: data[0],
        btns: (((data[1] << 8) | data[2])) & 0x1f9f,
        leds: data[3] & 0xf0,
        flags: data[3] & 0x0f,
        battery: data[6]
      };
      return report;
    }else
    if( data[0] == WIIREMOTE_REPORTID_READ_DATA ){
・・・

レポーティングモード設定する関数は以下の通りです。

wiiremocon.js
  setDataReportingMode(mode){
    var param = Buffer.alloc(4);
    param.writeUInt8(0xa2, 0);
    param.writeUInt8(WIIREMOTE_REPORTID_REPORTINGMODE, 1);
    param.writeUInt8(0x00, 2);
    param.writeUInt8(mode, 3);

    console.log('setDataReportingMode:' + param.toString('hex'));
    return this.l2cap.write(0, param);
  }

拡張コントローラの有効化

Wiiヌンチャクは、Wiiリモコンに接続して使います。
拡張コントローラと呼ばれていて、利用するには有効化が必要です。

wiiremocon.js
  async enableExtension(enable){
    if( enable ){
      await this.writeRegister(0xa400f0, Buffer.from([0x55]));
      await this.writeRegister(0xa400fb, Buffer.from([0x00]));
    }else{
      await this.writeRegister(0xa400f0, Buffer.from([0x00]));
    }
  }

Wiiremote/Extension Controllers のThe New Wayのところです。
 http://wiibrew.org/wiki/Wiimote/Extension_Controllers#Identification

拡張コントローラのイベント内容

ヌンチャクやバランスボードのイベントの内容は以下にあります。

・ヌンチャク
 http://wiibrew.org/wiki/Wiimote/Extension_Controllers/Nunchuck#Data_Format

・バランスボード
 http://wiibrew.org/wiki/Wii_Balance_Board#Data_Format

パースする関数は、parseExtension(type, data) です。

バランスボードに関しては、データレポーティングを解釈するために準備が必要です。
データレポーティングは、バランスボードの四隅のセンサから受信したデータの生値であり、重量に換算するにはキャリブレーションが必要です。

キャリブレーションは、以下に示すレジスタに記録されています。
 http://wiibrew.org/wiki/Wii_Balance_Board#Calibration_Data

ということで、あらかじめreadBalanceBoardCalibration() でキャリブレーションを読み出しておいて、データレポーティングとキャリブレーションを使って、calcurateBalanceBoard(data, base) で補正することで初めて、バランスボードの各センサでの重量を認識することができます。

ちなみに、バランスボードはWiiリモコンとしてふるまいます。ですので、バランスボードとWiiリモコンを同時に使う場合には別インスタンスを立ち上げる必要があります。

wiiremocon.jsの使い方まとめ

〇準備

const WiiRemocon = require('./wiiremocon');
const wii = new WiiRemocon();

〇connect(addr, retry = 2)
Wiiリモコンまたはバランスボードへの接続

〇disconnect()
Wiiリモコンまたはバランスボードとの切断

〇readBalanceBoardCalibration()
バランスボードのキャリブレーションの取得

〇calcurateBalanceBoard(data, base)
バランスボードの各センサの重量の計算

〇parseExtension(type, data)
受信イベント内の拡張コントローラ情報の解釈。typeに指定できるのはWIIREMOTE_EXT_TYPE_NUNCHUCKまたはWIIREMOTE_EXT_TYPE_BALANCEBOARDのみ。

〇parseReporting(data)
受信イベントの解釈

〇setLed(led_mask, led_val)
WiiリモコンのLEDの点灯。WIIREMOTE_LED_BIT0~WIIREMOTE_LED_BIT4までのOR指定。

〇setRumble( rumble )
Wiiリモコンの振動の有効化

〇setDataReportingMode(mode)
レポーティングモードの設定。WIIREMOTE_REPORTID_XXXX を指定します。

〇requestStatus()
ステータス情報のデータレポーティングの要求
以下のレポートを要求します。
http://wiibrew.org/wiki/Wiimote#0x20:_Status

〇enableExtension(enable)
拡張コントローラの有効化

MQTTに接続

今度は、データレポーティングをMQTTにPublishしてみましょう。そうすることで、いろんなクライアントがWiiリモコンを使えるようになります。

npmモジュールのmqttを使いました。

mqttjs/MQTT.js
 https://github.com/mqttjs/MQTT.js

MQTTのトピックとして、データレポーティングなどのWiiリモコンから見てOut方向のトピックと、クライアント側からの要求を受け付けるIn方向のトピックの2つを使います。

Out方向のトピックは、ほぼデータレポーティングなのであまり説明はいりませんが、In方向のトピックについて補足します。

Wiiリモコンとの接続やレポーティングモードの設定などは、クライアント側からの要求によって開始します。そのためのトピックです。
以下の要求を受け付けられるようにしてみました。コマンドコード的なもので区別しています。

〇WIIREMOTE_CMD_CONNECT
Wiiリモコンやバランスボードと接続します。BTアドレスを引数として受け取ります。

〇WIIREMOTE_CMD_DISCONNECT
Wiiリモコンやバランスボードと切断します。

〇WIIREMOTE_CMD_WRITE
レポートIDに対する書き込みをします。何を書くかは、クライアント側で制御します。

〇WIIREMOTE_CMD_ENABLE_EXTENSION
拡張コントローラを有効化します。

〇WIIREMOTE_CMD_REQ_REMOTE_ADDRESS
接続したWiiリモコンやバランスボードのBTアドレスを取得します。結果は、データレポーティングとして返ってきます。

〇WIIREMOTE_CMD_READ_REG
Wiiリモコンのレジスタから読み出しします。結果は、データレポーティングとして返ってきます。

〇WIIREMOTE_CMD_WRITE_REG
Wiiリモコンのレジスタに書き込みをします。

〇WIIREMOTE_CMD_REQ_STATUS
ステータス情報のデータレポーティングを要求します。結果は、データレポーティングとして返ってきます。

〇WIIREMOTE_CMD_READ_REG_LONG
Wiiリモコンのレジスタから読み出しします。WIIREMOTE_CMD_READ_REG と同様ですが、そちらは最大16バイトまでの読み出しに対し、こちらはそれ以上の長さを読み出します。内部で16バイト読み出しを繰り返しています。

ソースコード

MQTTに接続する部分のソースコードを示します。
npmモジュールのmqttとdotenvを使っています。

index.js
'use strict';

const WiiRemocon = require('./wiiremocon');
const mqtt = require('mqtt');
require('dotenv').config();

const MQTT_HOST = process.env.MQTT_HOST || '【MQTTブローカのURL】';
const MQTT_CLIENT_ID = process.argv[2] || '【クライアントID】';
const MQTT_TOPIC_CMD = process.argv[3] || '【In方向のトピック名】';
const MQTT_TOPIC_EVT = process.argv[4] || '【Out方向のトピック名】';
console.log("MQTT_CLIENT_ID: " + MQTT_CLIENT_ID);
console.log("MQTT_TOPIC_CMD: " + MQTT_TOPIC_CMD);
console.log("MQTT_TOPIC_EVT: " + MQTT_TOPIC_EVT);

const WIIREMOTE_CMD_EVT = 0x00;
const WIIREMOTE_CMD_ERR = 0xff;
const WIIREMOTE_CMD_CONNECT = 0x01;
const WIIREMOTE_CMD_DISCONNECT = 0x02;
const WIIREMOTE_CMD_WRITE = 0x03;
const WIIREMOTE_CMD_ENABLE_SOUND = 0x04;
const WIIREMOTE_CMD_ENABLE_EXTENSION = 0x05;
const WIIREMOTE_CMD_REQ_REMOTE_ADDRESS = 0x06;
const WIIREMOTE_CMD_READ_REG = 0x07;
const WIIREMOTE_CMD_WRITE_REG = 0x08;
const WIIREMOTE_CMD_REQ_STATUS = 0x09;
const WIIREMOTE_CMD_READ_REG_LONG = 0x0a;

var g_address = null;

const wii = new WiiRemocon();
const client = mqtt.connect(MQTT_HOST, { clientId: MQTT_CLIENT_ID });

client.on('connect', () => {
  console.log('mqtt.connected.');
  client.subscribe(MQTT_TOPIC_CMD, (err, granted) =>{
    if( err ){
      console.error(err);
      return;
    }
    console.log('mqtt.subscribed.');
  });
});

client.on('message', async (topic, message) =>{
  console.log('on.message', 'topic:', topic, 'message:', message.toString());
  try{
    var msg = JSON.parse(message);
    var cmd = msg.cmd;
    if( cmd == WIIREMOTE_CMD_CONNECT ){
      if( g_address ){
        await wii.disconnect();
        g_address = null;
      }
      var address = Uint8Array.from(msg.address);
      console.log(address);
      await wii.connect(address, msg.retry);
      g_address = address;
    }else
    if( cmd == WIIREMOTE_CMD_DISCONNECT ){
      if( g_address ){
        await wii.disconnect();
        g_address = null;
      }
    }else
    if( cmd == WIIREMOTE_CMD_WRITE ){
      await wii.writevalue(Buffer.from(msg.value));
    }else
    if( cmd == WIIREMOTE_CMD_ENABLE_SOUND ){
      await wii.enableSound(msg.enable);
    }else
    if( cmd == WIIREMOTE_CMD_ENABLE_EXTENSION ){
      await wii.enableExtension(msg.enable);
    }else
    if( cmd == WIIREMOTE_CMD_REQ_REMOTE_ADDRESS ){
      var message = {
        rsp: WIIREMOTE_CMD_REQ_REMOTE_ADDRESS,
      };
      if( g_address )
        message.address = [...g_address];
      client.publish(MQTT_TOPIC_EVT, JSON.stringify(message));
    }else
    if( cmd == WIIREMOTE_CMD_READ_REG ){
      var data = await wii.readRegister(msg.offset, msg.len);
      var message = {
        rsp: WIIREMOTE_CMD_READ_REG,
        offset: offset,
        data: [...data]
      }
      client.publish(MQTT_TOPIC_EVT, JSON.stringify(message));
    }else
    if( cmd == WIIREMOTE_CMD_WRITE_REG ){
      await wii.writeRegister(msg.offset, Uint8Array.from(msg.data));
    }else
    if( cmd == WIIREMOTE_CMD_REQ_STATUS ){
      var result = await wii.requestStatus();
      var message = {
        rsp: WIIREMOTE_CMD_REQ_STATUS,
        status: [...result]
      }
      client.publish(MQTT_TOPIC_EVT, JSON.stringify(message));
    }else
    if( cmd == WIIREMOTE_CMD_READ_REG_LONG ){
      var result = await wii.readRegisterLong(msg.offset, msg.len);
      var message = {
        rsp: WIIREMOTE_CMD_READ_REG_LONG,
        offset: result.offset,
        value: [...result.value]
      }
      client.publish(MQTT_TOPIC_EVT, JSON.stringify(message));
    }else{
      throw "Unknown cmd";
    }
  }catch(error){
    console.error(error);
    var message = {
      rsp: WIIREMOTE_CMD_ERR,
      error: error
    }
    client.publish(MQTT_TOPIC_EVT, JSON.stringify(message));
  }
});

async function wiiremote_mqtt(){
  wii.on("data", data =>{
    console.log(data);
    var message = {
      rsp: WIIREMOTE_CMD_EVT,
      evt: [...data]
    }
    client.publish(MQTT_TOPIC_EVT, JSON.stringify(message));
  });

  wii.on("error", data =>{
    console.error("Error", data);
    wii.disconnect();
    var message = {
      rsp: WIIREMOTE_CMD_ERR,
      error: data
    }
    client.publish(MQTT_TOPIC_EVT, JSON.stringify(message));
  });
}

wiiremote_mqtt()
.catch(error =>{
  console.error(error);
  client.end();
});

以下の部分は、環境に合わせて変更してください。

【MQTTブローカのURL】
例:mqtt://test.sample.com:1883
【クライアントID】
例:server
【In方向のトピック名】
 例:testwii_cmd
【Out方向のトピック名】
 例:testwii_evt

※複数同時に立ち上げる場合は、クライアントID、In方向/Out方向のトピック名は被らないようにしてください。

起動方法です。

$ node index.js
または
$ node index.js [クライアントID] [In方向のトピック名] [Out方向のトピック名]
MQTT_CLIENT_ID: [クライアントID]
MQTT_TOPIC_CMD: [In方向のトピック名]
MQTT_TOPIC_EVT: [Out方向のトピック名]
mqtt.connected.
mqtt.subscribed.

補足

MQTTブローカの立ち上げについては以下を参考にしてください。ブラウザから接続する場合には、WebSocket接続も有効にする必要があります。
 AWS IoTにMosquittoをブリッジにしてつなぐ

以上

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

node.js + GitHub + Travis CI + Code ClimateでCI入門

はじめに

この記事は、JavaScript開発でCI環境を導入するためのガイドです。
もしCI環境の導入に興味を持ったら、この記事を土台にみなさまの環境にあったCI/CDへ発展させてください。

対象となる読者

  • JavaScriptで開発をしている
  • CIという言葉を聞いたことがある
  • テスティングフレームワークを使ったことがない
  • バグ修正をしたら別の箇所でバグが出た
  • 依存パッケージの更新作業に負担を感じる

この記事は、テスト自動化やCIに興味はあるが、まだ導入したことがない開発者を対象としています。

対象とする環境

  • node.js 12.18.3
  • jest 26.0

CIとは / テスト自動化とは

CI (Continuous Integration / 継続的インテグレーション)とは、短期間で開発ブランチを統合し続ける開発手法です。グループ開発では、開発者がそれぞれ作業ブランチを抱えます。その作業ブランチは、定期的に統合しないと細分化し続けます。細分化しすぎたブランチは統合に膨大な作業が必要になるインテグレーション地獄を引き起こします。このインテグレーション地獄を回避するために発案された手法がCIです。

テスト自動化とは、ソフトウェアによってテストの設計、実行、報告を支援する取り組みです。テスト自動化に必要なソフトウェアをセットにしたものがテスティングフレームワークです。CIにおけるテストは、作業ブランチが正常に統合できているかを判定します。

テスト自動化はCIを実現するために必須の要素です。CIにおけるテストツールは、開発ブランチが統合されるたびにテストを自動実行し結果を報告し続けます。CIに必要な回数のテストを手動で実行するのは、現実的ではありません。

テスト自動化はCIの実施に必須の要素です。そのためCI環境には必ずテストツールが含まれます。

テスト自動化の利点

テスト自動化には、以下のような利点があります。

  • リグレッションの防止
  • 複数環境での動作確認
  • CD(継続的デリバリー / 継続的デプロイメント)への発展

リグレッションの防止

プログラムを修正すると、思わぬ箇所に影響しバグを発生させてしまう危険性があります。修正により取り去ったはずのバグが再発したり、他の機能が正常に働かなくなることをリグレッションデグレードといいます。

テスト自動化はリグレッションの発生を早期に発見し、問題箇所を特定するのに役立ちます。その結果、新機能開発や機能修正の負担を減らします。

プログラムが依存しているnpmモジュールの更新も、リグレッションやデグレードを引き起こします。依存関係が複雑なほどモジュール更新によるリグレッションの発見は難しくなります。テスト自動化はこうした問題の発見と修正作業の負担を減らします。

複数環境での動作確認

複数のプラットフォームをサポートするプログラムの品質を維持するのは大変な作業です。さらにnode.jsの複数のバージョンをサポートすると、テストの工数は掛け合わせで増えていきます。

テストツールは、動作環境の構築を自動化します。そのためテスト工数を圧縮できます。

CD(継続的デリバリー / 継続的デプロイ)への発展

CI環境は、CD(継続的デリバリー / 継続的デプロイメント)へ発展できます。

継続的デリバリーは、テストを通じでソフトウェアを本番環境へ展開可能か判定する開発手法です。継続的デプロイメントは、テストに合格したコードを自動的に本番環境へ展開する開発手法です。CDはソフトウェア開発と運用を一体化し、ソフトウェアの更新頻度を向上させます。

CI環境の構成

本記事では、以下のサービスを組み合わせてCI環境を構築します。

  • リモートリポジトリ : GitHub
  • テスティングフレームワーク : jest
  • CIサービス : Travis CI
  • コードカバレッジ収集 : Quality By Code Climate

jest

jestはJavaScriptテスティングフレームワークです。テストファイルの作成、実行、テスト網羅率のレポートまでを1つのフレームワークでカバーします。今回はjestを最小限の構成で導入する方法をご紹介します。

インストール

Getting Started

最初に、jestをインストールします。

npm install --save-dev jest

テストファイルを書く

つぎにテストファイルを作成します。

プロジェクトルートにsum.jsというJavaScriptファイルがあり、その中にsumという関数があるとします。同じディレクトリにtest.spec.jsを作成し、sum関数をテストします。

sum.js
function sum(a, b) {
  return a + b;
}
module.exports = sum;
test.spec.js
const sum = require('./sum');

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});

sum関数に引数1,2を与え、戻り値が3であればテスト成功、それ以外ならテスト失敗となります。

テストの実行

最後に、package.jsonにテスト実行コマンドを追加します。

package.json
{
  "scripts": {
    "test": "jest"
  }
}

npm run testでコマンドを実行すれば、テストが実行されて結果が報告されます。

より詳しい導入方法は、こちらの記事を参照してください。

Travis CI

Travis CIとは、リポジトリの監視、テストの実行、結果の報告を行うCIサービスです。さまざまな言語 / OS / テスティングフレームワークに対応しており、今回はnode.jsとjestの組み合わせでテストを行います。

Travis CIはバプリックリポジトリでは無料で利用できます。プライベートリポジトリのテストは有料サービスとなります。

サインイン

まずはサインインページからGitHubアカウントでサインインします。

リポジトリの追加

つぎにトップページのプラスボタンを押して、テスト対象リポジトリを追加します。

パブリックリポジトリの一覧が表示されるので、チェックボックスをONにします。

これでTravis CIがリポジトリの監視を開始します。Travis CIはmasterブランチにコードがpushされると動き出します。その都度テストが実行され、結果がGitHubアカウントのメールアドレスに報告されます。

設定ファイル

Travis CIはリポジトリのルートディレクトリに配置された.travis.ymlという名前の設定ファイルにしたがって動作します。node.jsでテストを実行するには、以下の設定ファイルを追加します。

travis.yml
language: node_js
node_js:
  - "10"
  - "12"
  - "14"

language: node_jsを設定すれば、Travis CIがpackage.jsonを探してtestコマンドを実行します。node_jsの中にバージョン番号を追加すれば、Travis CIはそれぞれのnode.jsでテストを並列実行します。

.travis.ymlのより詳しい記述方法は、公式ドキュメントを参照してください。

ymlファイルに関する詳しい解説は、こちらの記事をご参照ください。

テストの実行

.travis.ymlをリモートリポジトリのmasterブランチにpushすれば、Travis CIがテストを開始します。

Travis CIは

  • 仮想マシンの起動
  • 言語環境のインストール
  • package-lock.jsonyarn.lockなどにしたがって依存モジュールをインストール
  • package.jsonのtestスクリプトを実行
  • テスト結果を報告

という手順でテストを実行します。

CIサービスは、テストの実行のたびに初期状態の仮想マシンを起動し、モジュールをインストールします。そのためローカルのテストでは発見しにくい、動作環境に依存するバグも発見できます。

すべてのテストをパスすれば、ビルドの状態を表すバッジの色が緑になります。

Quality By Code Climate

Travis CIには、テストがどれだけのコードを網羅しているかを表すコードカバレッジを表示する機能がありません。コードカバレッジが低すぎると、リグレッションの発生を見逃す可能性が高くなります。

Quality By Code Climateは、コード品質を監視、維持するためのオンラインサービスです。今回はこのサービスをTravis CIと連携させ、コードカバレッジを監視します。

Quality By Code Climateも、パブリックリポジトリでは無料で利用できます。

サインイン

Code Climateの「LOGIN」メニューからQUALITYサービスにログインします。

「Open Source」を選択し、サービスを開始します。

リポジトリの追加

つぎに、監視するリポジトリを追加します。「Add a repository」ボタンを押して、リストからパブリックリポジトリを選びます。

リポジトリの初回スキャンが始まり、しばらくするとコードの監視が始まります。

Test Report IDの取得

初期状態では、Test Coverageの欄が雨傘アイコンになっています。これはコードカバレッジの情報がないことを表します。

Travis CIとの連携には、Test Report IDが必要になります。下のスクリーンショットを参考に、IDを取得してください。

このIDはCode Climateにコードカバレッジ情報を送信する書き込み専用キーです。パブリックリポジトリに公開しても問題ありません。(公式ドキュメント

Travis CIとの連携

testコマンドに、カバレッジ出力オプションを追加します。

package.json
{
  "scripts": {
    "test": "jest --coverage"
                  ^^^^^^^^^^
  }
}

Test Report IDをTravis CIに渡すため、.travis.ymlを書き換えます。

travis.yml
env:
  global:
    - CC_TEST_REPORTER_ID=【ここにTest Report ID】
language: node_js
node_js:
  - "10"
  - "12"
  - "14"
before_script:
  - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
  - chmod +x ./cc-test-reporter
  - ./cc-test-reporter before-build
after_script:
  - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT

参考 CodeClimate : Travis CI Test Coverage

Test Report IDを環境変数にセットします。テスト前に報告用アプリケーションをダウンロード、テスト後にCode Climateへ結果を送信します。

このpackage.json.travis.ymlをmasterブランチにプッシュします。

確認

Quality By Code Climateのページへ戻り、雨傘アイコンを確認します。
雨傘アイコンがパーセンテージに変更されていれば、無事に設定完了です。

個人的な感想

GitHub Actionsの登場によりCI/CD環境の選択肢が大きく広がりました。CI環境を小さく導入することで、こうした動きをより理解しやすくなります。

以上、ありがとうございました。

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

AWSサーバレスで(SPAではなく)画面遷移型のWebアプリをつくる

経緯

AWSサーバレスを採用してWebアプリ(画面)を作ることになりました。コンシューマ(一般ユーザ)向けの画面ではなく、企業向けの管理画面です。

メンバーの皆さんにReactとかを学んでいただく時間的な余裕はなかったため、SPAではなく、メンバーの皆さんに経験のある「画面遷移型」の構成にしました。

ただ、AWSサーバレスで画面遷移型のWebアプリを作る、という事例を見つけることができず、実現方式をあれこれ考える必要がありました。構成が固まるまでに悩んだことや、自分なりの解を記事にすることで、同じようなことに悩まれている方のヒントになればと思ってます。

アーキテクチャ

スクリーンショット 2020-08-08 21.04.25.png

ポイントは以下のとおりです。

  • Lambdaではaws-serverless-expressを採用しました。エンドポイントごとにLambda関数を定義する必要がなくなるとともに、ExpressのノウハウやNPMライブラリを活用できるためです。
  • テンプレートエンジンにはpug.jsを採用しました。Expressのテンプレートエンジンとしてデフォルト採用されているためです。初めて使ってみましたが、簡潔にコーディングできるので使いやすいと感じました。

シーケンス

①ログイン画面の表示

aa.png

こちらについては特筆すべきことはありません。express-sessionなどについては後述します。

②ログイン処理

seq.png

ポイントは以下のとおりです。

Cognitoでの認証

  • ユーザープール認証フローに沿って、ユーザの認証を行います。公式ドキュメントの通りに、ブラウザ内のJavaScriptからCognitoにID/パスワードを送信します。公式ドキュメントに記載されている「AmazonCognitoIdentity」を利用するには、こちらの手順に従ってamazon-cognito-identity-jsを読み込む必要があります。
  • Cognitoでの認証が成功すると、CognitoからIDトークンとアクセストークンが返却されます。今回は認証をしたいので、IDトークンを利用します。ブラウザ内のJavaScriptから、IDトークンをAPI gatewayに送ります(画面遷移型なので、FormをSubmitします)。
  • 一方、ユーザープール認証フローの他に、OpenID Connectによる認証フローも用意されています。その場合、Cognitoのログインエンドポイントを使うことで、ログイン画面のUIすら開発しなくてもよくなります。ただ、ログインエンドポイントから返されるログイン画面には、英語のデフォルト文言をカスタマイズできない、という致命的なデメリットがあります。カスタマイズできるのはCSSでのスタイル定義のみです。今回の案件の場合、さすがに英語のデフォルト文言ではNGでしたので、ログインエンドポイントの利用を諦めました。

IDトークンの検証

  • ブラウザから送信されたIDトークン(JWT)を検証します。Express側では、送信されてきたIDトークンが、正当なユーザから送信されたものか、あるいは攻撃者によって偽装されたものなのか、検証する必要があります。そこで、(図では記載を省略してますが)jwks-rsaを利用して、Cognitoの公開鍵でIDトークンの署名を検証します。
  • その他、有効期限が切れてないか、などの点をjsonwebtokenを使って検証します。
  • Cognitoが発行するIDトークンには、以下のとおりユーザの属性が含まれています。ログイン時には、これらの情報をセッションに格納し、次のリクエストで参照できるようにしておきます。
    • 「cognito:groups」クレームに、そのユーザがどのCognitoグループに属するか、という情報が入っています。詳しくはこちらを参照。
    • 「custom:~~」に、カスタム属性が入っています。ここに、例えば顧客企業のIDなど、業務処理で必要なデータを設定できます(Cognitoにユーザーを登録するときに、設定されるようにしておきます)。

セッション情報の管理

  • セッション情報の管理には、express-sessionを使います。Express界隈でのデファクトみたいですね。Expressのミドルウェア(共通処理)として動作します。セッション情報の管理(作成、取得、削除など)をしっかりやってくれるので、とても便利です。
  • express-sessionはセッションの保存先(ストア)の実装を持っておらず、ストアへのアクセス部分は別のライブラリが担当します。今回はセッションのストアとしてDynamoDBを利用したかったので、この「別のライブラリ」としてconnect-dynamodbを採用します。
  • DynamoDBに、セッション情報を保存するテーブル(セッション管理TBL)を定義する必要があります。詳しくは、connect-dynamodbのドキュメントを参照してください。

セッションIDの返却

  • セッション情報が新規に生成されると、express-sessionによってセッションIDが採番されます。このセッションIDをCookieに保存してブラウザに返却します。この時、(常識ですが)CookieにSecure属性を付与する必要があります。ただ、今回の構成の場合、aws-serverless-expressがプロキシの役割を果たすため、aws-serverless-express ⇔ Express間はhttp通信となります。このため、ExpressでSecure属性を付与すると、http通信なのでCookieが欠落した状態でレスポンスが送信されます。これを回避するには、app.set()でtrust proxyを設定する必要があります。今回はLambda内のaws-serverless-expressからしかExpressは呼ばれないので、単にapp.set('trust proxy', true))と設定しちゃいました。
  • (これもまた常識ですが)CookieにはHttpOnly属性を必ず付けましょう。express-sessionの設定で制御可能です。デフォルト設定はONなので、知らなくても問題ないかもしれませんが。

③ログイン後の画面遷移(認証・認可チェック)

seq.png

ポイントは以下のとおりです。

認証チェック

  • ログイン時にセッションに格納しておいたユーザIDをreq.sessionから取得します。以下の場合、未認証と見なすべきです。いずれの場合もif(req.session.userId)という感じで判断できます。
    • そもそも、セッションIDが送られていない場合。この場合、req.sessionにSessionオブジェクトが生成されます(この時のSessionオブジェクトには、空のCookieしか入ってません)。
    • セッションIDは送信されているが、セッション管理TBLに対応する項目(レコード)が無い場合や有効期限が切れている場合。
  • 未認証の場合、ログイン画面にリダイレクトします。
  • これらの処理は、Expressのミドルウェアとして実施します。

認可チェック

  • ログイン時にセッションに格納しておいたCognitoグループ(IDトークンのcognito:groupsクレームに入っていたもの)をreq.sessionから取得します。req.originalUrlからアクセス対象のパスを取得します。ユーザが属するグループに、そのパスを実行する権限があるかを判定し、権限がなければエラー画面を表示します。どのグループにどのパスのどのメソッドの実行が許可されるのか、といった定義については、今回は設定ファイルにベタ書きしちゃいました。

その他

バリデーション

単項目のバリデーションには、express-validatorを利用します。

実戦でこれを使うには、色々と工夫が必要です。最終的には以下のようになりました。

router

// 商品登録処理
router.post('/registerItem', validator.forRegisterItem, controller.registerItem);
  • 単項目のバリデーションについてはvalidatorにまとめて実装します。可読性を高めるためです。

validator

const { required, maxLength, alphanumeric } = require('../resources/message').BizError.SingleItemValidationError;

// 画面から入力されるのは、itemId(商品ID)、itemName(商品名)とします。
exports.forRegisterItem = [
  body('itemId', required).isLength({ min: 1 }),
  body('itemId', alphanumeric).isAlphanumeric(),
  body('itemId', maxLength({ max: 10 })).isLength({ max: 10 }),
  body('itemName', required).isLength({ min: 1 }),
  body('itemName', maxLength({ max: 10 })).isLength({ max: 10 }),
];
  • エラーメッセージの定義を共通化するため、messageに文言を定義します。
  • trimやescape(サニタイジング)といった処理は、以下のようにExpressミドルウェアで共通処理として定義します。
app.use([body('*').trim().escape(), query('*').trim().escape(), param('*').trim().escape()]);

message

const BizError = {
  SingleItemValidationError: {
    /** 必須エラー */
    required: '必須項目です。',
    /** 英数字以外が入力された場合のエラー */
    alphanumeric: '英数字で入力してください。',
    /** 桁数上限エラー */
    maxLength: ({ max }) => `${max}文字以下で入力してください。`,
  },
  // 以下、略。
  • 「●●文字以下で入力してください」といったように、●●の部分を可変にできるようにすべきです。そこで、maxLengthは関数として定義しています。

controller

// 商品登録
exports.registerItem = commonLayer.wrap(async (req, res) => {
  const itemId = req.body.itemId;
  const itemName = req.body.itemName;

  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    const errMsgs = validationUtil.groupMsgsByProp(errors);
    res.render('customer/registerItem', { ...errMsgs, itemId, itemName });
    return;
  }

  // 後続処理
});
  • validationUtilでは、pugで入力項目の近くにエラーメッセージを表示するためにひと工夫をしています。
  • バリデーションとは関係ないですが、commonLayer.wrap()でやっていることはこちらの記事と同じです。

validationUtil

exports.groupMsgsByProp = (errors) => {
  const errorsMappings = errors.errors.reduce((prev, current) => {
    if (!prev[current.param]) {
      prev[current.param] = [];
    }
    prev[current.param].push(current);
    return prev;
  }, {});

  return {
    'errorMappings': errorsMappings,
  };
};

pug

registerItem.pug
extends ../common/layout.pug

block title
  title 商品登録

block content
  .container.mt-5
    .d-flex.justify-content-center
      .col-8
        if successMsg
          p.text #{successMsg}
        +globalErrMsg()
        form(method="post")
          .form-group
            label(for="itemId") 商品ID
            input#company_id.form-control(type="text", name="itemId", value=itemId)
            +errMsgsOf('itemId')
          .form-group
            label(for="itemName") 商品名
            input#company_id.form-control(type="text", name="itemName", value=itemName)
            +errMsgsOf('itemName')
          input.btn.btn-primary(type="submit", value="登録")
common/layout.pug
mixin errMsgsOf(propName)
  if errorMappings && errorMappings[propName]
    each error in errorMappings[propName]
      div #{error.msg} 

Expressミドルウェア設定

  • helmetnoCacheを利用して、レスポンスヘッダーを設定します。これにより、XSSなどの対策を行い、セキュリティレベルを高めます。
  • その他、セキュリティについては、Express公式サイトでの解説をしっかり把握しておくのが良いです。

おわりに

新たな知見が得られましたら、今後も更新していきたいと思います。

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

【GCP】【Firebase】 「Could not load the default credentials.」と出た場合の対処

概要

Google Cloud Functions(Node.js)を実行したときに、GoogleAuthで「Could not load the default credentials.」と出た場合の対処方法

コード

こんな感じのコードで、

  const { google } = require('googleapis');
  const auth = new google.auth.GoogleAuth({
    scopes: ['https://www.googleapis.com/auth/cloud-platform'],
  });
  const authClient = await auth.getClient();
  google.options({ auth: authClient });

こんな感じのエラーが出る場合、

Error: Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information.
    at GoogleAuth.getApplicationDefaultAsync (/srv/node_modules/googleapis-common/node_modules/google-auth-library/build/src/auth/googleauth.js:155:19)
    at <anonymous>
    at process._tickDomainCallback (internal/process/next_tick.js:229:7) 

環境変数を設定する方法もあるが、
鍵ファイル(JSON)のパスをGoogleAuthインスタンス生成時に直接指定する方法でも解消可能
(鍵ファイルは適切なロールが付与されたサービスアカウントで生成したもの)

  const { google } = require('googleapis');
  const auth = new google.auth.GoogleAuth({
    keyFilename: './key.json',
    scopes: ['https://www.googleapis.com/auth/cloud-platform'],
  });
  const authClient = await auth.getClient();
  google.options({ auth: authClient });

※鍵ファイルは、functionsディレクトリ直下に置いてcloud functionsにデプロイ

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

Laravel npm run devでエラーが発生した話

目的

  • npmを用いて必要パッケージを取得後に$ npm run devを実行したらエラーが発生した話をまとめる

実施環境

  • ハードウェア環境
項目 情報
OS macOS Catalina(10.15.5)
ハードウェア MacBook Pro (13-inch, 2020, Four Thunderbolt 3 ports)
プロセッサ 2 GHz クアッドコアIntel Core i5
メモリ 32 GB 3733 MHz LPDDR4
グラフィックス Intel Iris Plus Graphics 1536 MB
  • ソフトウェア環境
項目 情報 備考
PHP バージョン 7.4.3 Homwbrewを用いて導入
Laravel バージョン 7.0.8 commposerを用いてこちらの方法で導入→Mac Laravelの環境構築を行う
MySQLバージョン 8.0.19 for osx10.13 on x86_64 Homwbrewを用いてこちらの方法で導入→Mac HomebrewでMySQLをインストールする
Node.jsバージョン 14.6.0 こちらの方法で導入した→AWS EC2 AmazonLinux2 Node.jsをインストールしてnpmコマンドを使用できる様にする
npmバージョン 6.14.6 こちらの方法で導入した→AWS EC2 AmazonLinux2 Node.jsをインストールしてnpmコマンドを使用できる様にする

エラーの原因

  • $ npm installを実行し忘れていた。

問題までの経緯

  1. 下記で紹介されている内容を実施したくてLaravelアプリを新規作成した。
  2. アプリ名ディレクトリに移動後、下記コマンドを実行してパッケージインストールを行った。

    $ npm install simple-peer --save-dev
    $ npm install pusher-js --save-dev
    
  3. アプリ名ディレクトリで下記コマンドを実行してのbootstrap.jsを開く。

    $ vi resources/js/bootstrap.js
    
  4. 下記の内容を追記する。

    resources/js/bootstrap.js
    window.Peer = require('simple-peer');
    window.Pusher = require('pusher-js');
    
  5. アプリ名ディレクトリで下記コマンドを実行してapp.jsを開く。

    $ resources/js/app.js
    
  6. 下記の記載をコメントアウトした。

    1. 修正前

      resources/js/app.js
      const app = new Vue({
        el: '#app'
      });
      
    2. 修正後

      resources/js/app.js
      //const app = new Vue({
      //  el: '#app'
      //});
      
  7. アプリ名ディレクトリで下記コマンドを実行してビルドを試みた。

    $ npm run dev
    

問題

  1. 下記のエラーが発生した。

    > @ dev /var/www/html/pusher_video
    > npm run development
    
    > @ development /var/www/html/pusher_video
    > cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js
    
    sh: cross-env: command not found
    npm ERR! code ELIFECYCLE
    npm ERR! syscall spawn
    npm ERR! file sh
    npm ERR! errno ENOENT
    npm ERR! @ development: `cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js`
    npm ERR! spawn ENOENT
    npm ERR! 
    npm ERR! Failed at the @ development script.
    npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
    
    npm ERR! A complete log of this run can be found in:
    npm ERR!     /home/ec2-user/.npm/_logs/2020-08-04T03_20_02_697Z-debug.log
    npm ERR! code ELIFECYCLE
    npm ERR! errno 1
    npm ERR! @ dev: `npm run development`
    npm ERR! Exit status 1
    npm ERR! 
    npm ERR! Failed at the @ dev script.
    npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
    
    npm ERR! A complete log of this run can be found in:
    npm ERR!     /home/ec2-user/.npm/_logs/2020-08-04T03_20_02_713Z-debug.log
    

問題解決までの経緯

  1. $ npm installを実行していなかったことに気が付き実行した。
  2. 再度$ npm run devを実行したところエラーは発生せず問題は解決した。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【GCP】【Firebase】 「Could not load the default credentials.」と出た場合のいち対処法

概要

Google Cloud Functions(Node.js)を実行したときに、GoogleAuthで「Could not load the default credentials.」と出た場合の対処方法

コード

こんな感じのコードで、

  const { google } = require('googleapis');
  const auth = new google.auth.GoogleAuth({
    scopes: ['https://www.googleapis.com/auth/cloud-platform'],
  });
  const authClient = await auth.getClient();
  google.options({ auth: authClient });

こんな感じのエラーが出る場合、

Error: Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information.
    at GoogleAuth.getApplicationDefaultAsync (/srv/node_modules/googleapis-common/node_modules/google-auth-library/build/src/auth/googleauth.js:155:19)
    at <anonymous>
    at process._tickDomainCallback (internal/process/next_tick.js:229:7) 

環境変数を設定する方法もあるが、
鍵ファイル(JSON)のパスをGoogleAuthインスタンス生成時に直接指定する方法でも解消可能
(鍵ファイルは適切なロールが付与されたサービスアカウントで生成したもの)

  const { google } = require('googleapis');
  const auth = new google.auth.GoogleAuth({
    keyFilename: './key.json',
    scopes: ['https://www.googleapis.com/auth/cloud-platform'],
  });
  const authClient = await auth.getClient();
  google.options({ auth: authClient });
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む