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

【Lambda】zipファイルの圧縮解凍サンプル【nodejs】

やること

Lambdaで
・S3バケットにzipファイルを作成する(つまり圧縮)
・S3バケットに格納されている、zipファイルから中身を取り出す(つまり解凍)

手順概要

  • node-zip が動くようにLayerファイル(zip)を作る
  • LambdaにLayerファイルをアップロードする
  • 関数を作る

Layerファイルを作ります

クライアントで作業します。

例:新規でAmazonLinux2 を作成して、nodejsをインストールからやります。
たったこれ?だけです。

curl --silent --location https://rpm.nodesource.com/setup_10.x | sudo bash -
yum install -y nodejs 
mkdir nodejs
cd nodejs
npm init -y
npm install -y node-zip
cd ..
zip -r layer.zip nodejs/.

zipをLambdaに登録します

AWS管理コンソールで作業します。
スクリーンショット 2019-09-29 21.43.44.png
「レイヤーの作成」で、先ほど作ったLayer.zipファイルを入れれば終わりです。

関数の準備

Layersを選択して
スクリーンショット 2019-09-29 21.48.17.png
「レイヤーの追加」押下で先ほど作ったレイヤーが出てきますので、選択。
スクリーンショット 2019-09-29 21.48.30.png

関数の作成

下のコードを先頭あたりに入れて、実行してみます。エラーが出なければレイヤーが効いています。

const node_zip = require('node-zip');

ちなみに、Layerを外すと、上記コードはエラーになります。

解凍

S3に置いてあるzipファイルを解凍します。
IAMで、s3:GetObject が必要です

抜粋
const aws = require('aws-sdk');
const s3 = new aws.S3({ apiVersion: '2006-03-01' });
const node_zip = require('node-zip');

s3.getObject({Bucket:s3bucket, Key:s3object},function(err,data) {
    if (err) {
       return;
    } else {
       var zip = new node_zip(data.Body, {base64: false, checkCRC32: true});
       for (var fileinzip in zip.files) {
           let pKey="";
           let pBody="";
           for (var para in zip.files[fileinzip]) {
               if(para=="name")   pKey = zip.files[fileinzip][para];
               if(para=="_data") pBody = zip.files[fileinzip][para];
           }
           // pKey にファイル名、pBodyにファイル内容が入っている
           // putObjectするとか。。
        }  // 次のファイル
    });

圧縮

圧縮対象のファイルはあらかじめ準備する必要はないです。
ファイル名とファイル内容があれば良いです。

IAMで、s3:PutObject が必要です

const node_zip = require('node-zip');
const zip = new node_zip();

zip.file("ファイル名1", "内容文字列1");
zip.file("ファイル名2", "内容文字列2");
zip.file("ファイル名3", "内容文字列3");

let zipContent = zip.generate({base64: false,compression:'DEFLATE'});
var buf = Buffer.from(zipContent, 'binary');

let param ={ Bucket:bucket, Key : "hoge.zip", Body: buf, ContentType: 'application/zip'};
s3.putObjct(param);

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

Node.js&google-home-notifierでGoogle Homeに喋らせよう

Node.jsを使って、Google Homeに喋らせました。
その際詰まったところなどを解説します。

1.google-home-notifierをインストール

$ npm init
$ npm install google-home-notifier

※npm initはデフォルトでいい場合は全てEnterする。

2.node.jsでサンプルコードを書く

sample.js

3.実行

$ node sample.js

これでいけるはず...

Error: get key failed from google

はまりました...orz

色々調べてこちらの対策を発見:pull request

修正するも、動かない...。

調べても解決に至らずだったので、

google-home-notifierのソースコードを確認してみる事にしました。

気になるところを発見!

node_modules/google-tts-api/lib/key.js
var match = html.match("TKK='(\\d+.\\d+)';");
if(!match) throw new Error('get key failed from google');

htmlが何かのhtmlを読んでmatchさせているが失敗している。
そして、恐らく、それが、

node_modules/google-tts-api/lib/key.js
var host = 'https://translate.google.com';

というわけで、
https://translate.google.com/

のhtmlを解析していきます。

1.右クリックで「ページのソースを表示」
2.command+fで「TKK検索」
3.気になる内容が引っかかる。

view-source
tkk:'436044.375069185'
TKK = mobileWebapp.tkk

そして、ソースコードと照らし合わせてみると、

node_modules/google-tts-api/lib/key.js
var match = html.match("TKK='(\\d+.\\d+)';");

恐らく以前は、
TKK='(\d+.\d+)'; //つまり、TKK=436044.375069185;数字は任意

と書いてあったけど、
今は、https://translate.google.com/ のhtmlコードが変わってしまい

view-source
tkk:'436044.375069185',
TKK = mobileWebapp.tkk

となり、TKK='(\d+.\d+)';にmatchしなくなった。
そのため、tkk:'436044.375069185',にmatchするように修正。

node_modules/google-tts-api/lib/key.js
//var match = html.match("TKK='(\\d+.\\d+)';");
var match = html.match("tkk:'(\\d+.\\d+)',");

動きました!

ちなみにソースコードはこちらにupしています。

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

Node.js&google-home-notifierでGoogle Homeに喋らせよう(Error解析付き)

Node.jsを使って、Google Homeに喋らせました。
その際詰まったところなどを解説します。

こちらのサイトを参考にnode.js作成
https://qiita.com/SatoTakumi/items/c9de7ff27e5b70508066

0.node.jsのバージョン

$ npm -v
  6.9.0
$ node -v
  v10.16.0

1.google-home-notifierをインストール

$ npm init
$ npm install google-home-notifier

※npm initはデフォルトでいい場合は全てEnterする。

2.node.jsでサンプルコードを書く

sample.js
   //ライブラリ参照
   var googlehome = require('./node_modules/google-home-notifier');
   // 言語設定
   var language = 'ja';
   // GoogleHomeのIPアドレスに書き換えてくださいね
   googlehome.ip('0.0.0.0',language);
   // 第一引数を自分のもっているGoogleHomeの名前に書き換えてくださいね
   //googlehome.device('googlehome', language);
   // Google Homeにしゃべって欲しい文章をここに記入してくださいね
   var text = 'こんにちは';
   // メイン処理
   try {
       googlehome.notify(text, function(notifyRes) {
           console.log(notifyRes);
       });
   } catch(err) {
       console.log(err);
   };

3.実行

$ node sample.js

これでいけるはず...

Error: get key failed from google

はまりました...orz

色々調べてこちらの対策を発見:pull request
修正するも、動かない...。
調べても解決に至らずだったので、

google-home-notifierのソースコードを確認してみる事にしました。

気になるところを発見!

node_modules/google-tts-api/lib/key.js
var match = html.match("TKK='(\\d+.\\d+)';");
if(!match) throw new Error('get key failed from google');

htmlが何かのhtmlを読んでmatchさせているが失敗している。
そして、恐らく、それが、

node_modules/google-tts-api/lib/key.js
var host = 'https://translate.google.com';

というわけで、
https://translate.google.com/

のhtmlを解析していきます。
スクリーンショット 2019-09-29 22.09.46.png

1.右クリックで「ページのソースを表示」
2.command+fで「TKK」検索
3.気になる内容が引っかかる。

view-source
tkk:'436044.375069185'
TKK = mobileWebapp.tkk

そして、ソースコードと照らし合わせてみると、

node_modules/google-tts-api/lib/key.js
var match = html.match("TKK='(\\d+.\\d+)';");

恐らく以前は、
TKK='(\d+.\d+)'; //つまり、TKK=436044.375069185;数字は任意

と書いてあったけど、
今は、
https://translate.google.com/ 
のhtmlコードが変わってしまい

view-source
tkk:'436044.375069185',
TKK = mobileWebapp.tkk

となり、TKK='(\d+.\d+)';にmatchしなくなった。
そのため、tkk:'436044.375069185',にmatchするように修正。

node_modules/google-tts-api/lib/key.js
//var match = html.match("TKK='(\\d+.\\d+)';");
var match = html.match("tkk:'(\\d+.\\d+)',");

動きました!

ちなみにソースコードはこちらにupしています。

これで喋らせる事ができましたが...

日本語を喋るけど、発音が英語っぽい日本語になる問題が発生。
修正方法:https://qiita.com/sohsatoh/items/69bcad398ffae11359f0

この部分を

google-home-notifier.js
var ip = function(ip) {
    deviceAddress = ip;
    return this;
}

こう直すと直るようです。

google-home-notifier.js
var ip = function(ip, lang = 'ja') {
    deviceAddress = ip;
    language = lang;
    return this;
}

完成!

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

ピクセルアートフレーム Divoom PIXOO で遊んでみた

はじめに

これは社内LTイベンント向けの資料です。


今日のお題「ガジェット」

divoom_pixoo.png


Divoom PIXOO

  • ハード
    • 16x16 の LEDパネル
      • スピーカーは無し(付いている機種もある)
    • Bluetooth接続
      • WiFiは無し
  • スマートフォンの専用アプリでコントロール
    • Divoom (iTunes)
    • 共有ギャラリーもある

Divoom アプリでできること

  • 静止画の描画、PIXOOへの転送
  • アニメーションの作成、PIXOOへの転送
  • テキストから、簡易電光掲示板の作成
  • つくった静止画/アニメーションの公開、他の人の作品のダウンロード
  • 組み込みアプリの利用(ここでデモ)
    • 時計、カレンダー、天気情報
    • 通知の連携
    • 各種VJ風アニメーション
    • 音のビジュアライズ

PIXOO ハック!


Node.js でやってみる

不安定だが、限定的に動作

  • 接続できないことがある
  • 一部の機能しか使えない

(1) 接続

const TIMEBOX_ADDRESS = "xx:xx:xx:xx:xx:xx"; // Bluetoothアドレス

const btSerial = new (require('bluetooth-serial-port')).BluetoothSerialPort();
const Divoom = require('node-divoom-timebox-evo');

btSerial.findSerialPortChannel(TIMEBOX_ADDRESS, function (channel) {
  btSerial.connect(TIMEBOX_ADDRESS, channel, function () {
    console.log('connected');
    btSerial.on('data', function (buffer) {
      console.log(buffer.toString('ascii'));
    });

    showTime(); // <-- ここのタイミングで、表示処理を呼び出す
    //sendImage();

  }, function () {
    console.log('cannot connect');
  });
}, function () {
  console.log('found nothing');
});

Node.jsで時刻を表示

(2) 時刻を表示

function showTime() {
  const d = new Divoom.TimeChannel;
  const buf = d.messages[0].asBinaryBuffer()[0];
  //console.log(buf);

  btSerial.write(buf,
    function (err, bytesWritten) {
      if (err) console.log(err);
      else {
        console.log('bytes sent=' + bytesWritten);
      }
      btSerial.close();
    }
  );
}

Node.jsでイメージを表示

(3) イメージ表示
なぜか、表示できるイメージとできないイメージがある

const imageFile = 'img16x16.png';
function sendImage() {
  const d = new Divoom.DisplayAnimation;
  d.read(imageFile)
    .then(result => {
      result.asDivoomMessages().forEach((message /*: TimeboxEvoMessage*/, index /*: number*/) => {
        const buf = message.asBinaryBuffer()[0];
        console.log(buf);

        btSerial.write(buf,
          function (err, bytesWritten) {
            if (err) console.log(err);
            else {
              console.log('bytes sent=' + bytesWritten);
            }
            btSerial.close();
          }
        );
      })
    })
    .catch(err => {
      console.error('read ERROR:', err);
    })
}

ハマりどころ1: デバイスのbluetoothアドレスが見つからない

  • スマートフォンとの接続を解除
  • Macに接続
    • システム環境設定 - Bluetooth - Pixoo に接続

system-bluetooth.png

  • Macで調べる
    • [Apple]メニュー - [このMacについて] - [概要]タブ - [シルテムレポート]ボタン
    • [ハードウェア] - [Bluetooth] を選び、右のリストで「Pixoo」のアドレスを探す

system-report.png


ハマりどころ2: サンプルコードが古い(動かない)

  • Divoomライブラリのソースコード自体は結構新しい
    • テストコードもある(シリアル通信は除く)
  • readme.md にあるサンプルの説明があってない(古い)
  • コードを読んでサンプルを直す
    • テストコードと、ライブラリの実装
    • 外部ライブラリ bluetooth-serial-port

まとめ

  • PIXOO ちょっとしたディスプレイ装置として、おもしろい
    • イベントごとの簡易電光掲示板にベスト
  • スマートフォンアプリから、素直に使える
  • ハックしても使えそう(が、まだ上手くは使えていない)
    • 通信の確立に失敗することがある
    • 動くコマンドと動かないコマンドがある
    • 表示できる画像と、表示できない画像がある(違いは不明)
    • → 気が向いたら調べます
  • 積みガジェットを消化するには、LTに取り上げるのが良い
    • 今回も買ったまま放置していたが、今回のネタとして調べてみた

おまけ: もう1つ買ったもの

  • 木製ブロック玩具「もくロック」
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.jsでゼロインストールを実現する

Node.jsのパッケージマネージャはnpmおよびその代替となるyarnが有名です。共に、次期バージョンアップ(npm tinkおよびyarn v2であるberry)ではゼロインストールを目指しています。

今回は、モジュールハックを使ってゼロインストールを実装してみました。

ゼロインストールとは

npm iyarnを実行するとnode_modules/以下にnpmが展開されます。node_modulesはブラックホールよりも深いと言われています。

node_modulesはブラックホールよりも深い

この反省点をもとにパッケージシステムそのものを否定したプロジェクトを、Node.jsのオリジナル作者であるryがdenoとして立ち上げております。

実際node_modulesはプロジェクトによっては簡単に数百MBytes以上の容量を食い散らかします。手元で様々なJavaScriptプロジェクトを動かしていると、128GBや256GB程度のSSDではいとも簡単にディスクフルを招きます。

そこで実行時にパッケージをfetchするかキャッシュからのみ読み込むことで、オンメモリでパッケージを解決するというアプローチがゼロインストールです。

ゼロインストールのためにパッケージをオンメモリに展開する

npmパッケージは、npmpkg.comやyarnpkg.comなどでホスティングされています(最近GitHubでもnpmやgemなどのホスティングサービスのbetaが始まっています)。

さて、npmpkg.com や yarnpkg.com では、パッケージ情報の取得やダウンロードだけならとても簡単です。所定のURLにGETでアクセスするだけで、jsonもしくはgzip/tarされたアーカイブを取得できます。

import fetch from 'node-fetch'

export const fetchPkg = async (name: string, version?: string) => {
  const url = `https://registry.yarnpkg.com/${name}`
  const json = await fetch(url).then(res => res.json())

  let pkgs: { [props: string]: { info: any; data: any } } = {}

  if (!version) {
    const { latest } = json['dist-tags']
    version = latest
  }

  const info = json.versions[version!]
  if (info.dependencies) {
    await Promise.all(
      Object.keys(info.dependencies).map(async name => {
        pkgs = {
          ...pkgs,
          ...(await fetchPkg(name, info.dependencies[name])),
        }
      }),
    )
  }

  const tarball = await fetch(info.dist.tarball).then(res => res.body)
  const data = await extractTar(tarball)
  pkgs[name] = { info, data }

  return pkgs
}

この TypeScript のコードでは register.yarnpkg.com から、情報(JSON)を取得してバージョンなどの情報を取得し、バージョンを指定していなければ最新版(latest)をダウンロードします。またdependencies も同様にダウンロードします。

やっていることは全体的には、async/await および Promise.all を使って、並列でパッケージダウンロードしているだけです。

最後のほうのconst data = await extractTar(tarball)では、取得した.tgzのtarballを展開しています。

この関数が返すデータは、パッケージ名をキーとして、infoプロパティにJSONデータ、dataプロパティにtarballを展開したものを格納しています。

tarballの展開

import zlib from 'zlib'

import tar from 'tar-stream'

const extractTar = (tarball: NodeJS.ReadableStream) => {
  return new Promise((resolve, reject) => {
    const files: { [props: string]: string } = {}
    const gunzip = zlib.createGunzip({})
    tarball
      .pipe(gunzip)
      .on('error', err => reject(err))
      .pipe(tar.extract())
      .on('error', err => reject(err))
      .on('finish', () => resolve(files))
      .on('entry', (headers, stream, next) => {
        let { name } = headers
        name = name.replace(/^package\//, '')
        if (!(name in files)) {
          files[name] = ''
        }
        stream.on('data', data => {
          files[name] += data.toString()
        })
        stream.on('error', err => reject(err))
        stream.on('end', () => next())
      })
  })
}

tarballはまずgzipで圧縮されたバイナリを伸張しなければいけません。それについては、Node.js zlib APIを使います。また Node.js では Node.js Stream API の Stream で大体のエコシステムができあがってしまっているため、全体を Stream 処理で行っています。

node-fetchres.bodyを使うとbodyStreamとして取得できるので、

    const gunzip = zlib.createGunzip({})
    tarball
      .pipe(gunzip)

で、前処理として、gzipの伸張を行います。これをさらにpipeで渡してtar-stream npm パッケージでエントリごとに取り出します。

このtar-streamはかなりクセが強い挙動を示すのでご注意ください。

      .pipe(tar.extract())
      .on('error', err => reject(err))
      .on('finish', () => resolve(files))
      .on('entry', (headers, stream, next) => {
        let { name } = headers
        name = name.replace(/^package\//, '')
        if (!(name in files)) {
          files[name] = ''
        }
        stream.on('data', data => {
          files[name] += data.toString()
        })
        stream.on('error', err => reject(err))
        stream.on('end', () => next())
      })

.pipe(tar.extract())で継続される Stream では通常とは異なりfinishイベントとentryイベントを使います。

entryイベントではヘッダ情報の取得と、データ本体の為の新規Streamを受け取ります。そのため、このようなストリーム処理のネストが発生します。データ本体のストリームは比較的素直なストリームです。

パッケージは、package/のprefixが付いてるため問答無用で剥がしておきます。

データストリームでは、dataイベントでデータをオンメモリに追加しておき、endイベントで、ストリームの次を促すためにnext()を実行します。

finishイベントが流れてくるとこれらの処理が一通り完了です。

今回はまとめて全部を1つのPromise化しているため、finish時に、resolveでPromiseを完了状態に持って行きます。

モジュールハックでオンメモリのパッケージを返すようにする

ここがゼロインストールの本領です。

その前にモジュールハックのコードを

export const Module = require('module') as any

export const originalLoad = Module._load

export const originalExts: { [props: string]: any } = {}

Object.keys(Module._extensions).forEach(ext => {
  originalExts[ext] = Module._extensions[ext]
})

type Hook = (m: any, filename: string) => any
export const hackExt = (ext: string, hook: Hook) => {
  Module._extensions[ext] = function(m: any, filename: string) {
    return hook(m, filename)
  }
}

export const builtinModules: string[] = Module.builtinModules

type Loader = (name: string, parent: any, isMain: boolean) => any

let loaders: Loader[] = []
export const hackLoader = (loader: Loader) => {
  loaders.push(loader)
  Module._load = function(name: string, parent: any, isMain: boolean) {
    for (const l of loaders) {
      const res = l(name, parent, isMain)
      if (res !== undefined) {
        return res
      }
    }
    return originalLoad(name, parent, isMain)
  }

  return () => {
    loaders = loaders.filter(l => l !== loader)
  }
}

モジュールハックのコードを汎用化したものです。今回は hackLoader関数を使います。

export const hackZeroinstall = (pkgs: any) => {
  const requireStack: [string, string][] = []

  const pkgNames = Object.keys(pkgs)
  if (pkgNames.length === 0) {
    return
  }
  console.log(`hackZeroinstall ${pkgNames}`)

ここでexportしているhackZeroinstallを、先ほどのfetchPkgで取ってきたpkgsデータをぶち込むと、オンメモリ展開されたパッケージを読み込むことができる、つまりゼロインストール処理が完成します。

requireStackは、ゼロインストール処理中のモジュール呼び出しスタックです。文字列2つのタプルの配列になっています。後ほど説明しますが、文字列は、モジュール名と、モジュールのパスです。

ここでは pkgs が空なら何もしません。

  const unhack = hackLoader((name, parent, isMain) => {
    let [modname, modpath] = name.split('/', 2)

    const isRelative = name.startsWith('.') || name.startsWith('/')
    if (requireStack.length === 0 && isRelative) {
      return
    }

ここでは、まずimportのソースパスやrequireの引数 name を '/' で区切ることによりモジュール名とパスを切り離します。

./で始まるローカルモジュールの場合でかつ、hack中でなければそのまま何もしません。

    if (isRelative) {
      ;[modname, modpath] = resolveRelative(requireStack, name)
    }

相対パスの場合、requireStack と新しいパス指定を元に、modnamemodpath を書き換えます。

const resolveRelative = (requireStack: [string, string][], name: string) => {
  const prev = requireStack[requireStack.length - 1]
  console.log('prev', prev)
  return [prev[0], path.join(path.dirname(prev[1]), name)]
}

まずスタックの1つ前を読み込みます。前述の通り、タプルの1つめはモジュール名 modname なのでそのまま返します。タプルの2つめはモジュールパスなので、path.dirname でパス名を取り出し、今回のパス指定を結合することで、パス解決を行います。

    if (!pkgNames.includes(modname)) {
      return
    }

ここまでの処理でmodnameは完全に確定しているため、modnameがもしpkgsの中 Object.keys(pkgs) になければ、hack処理をせずに帰ります。

    console.log('load hack:', name, modname, modpath)

    const filename = resolveFile(pkgs[modname], modpath)

次にファイル名を確定します。

const resolveFile = (pkg: any, modpath: string) => {
  let name = modpath || pkg.info.main

modpathが空の場合、package.infomainに書かれたモジュールを新たな名前として採用します。

  const exts = ['.js', '.json', '.node']

  if (exts.find(ext => name.endsWith(ext))) {
    return name
  }

modpathが拡張子で終わる場合はその拡張子をそのまま使えるためここでファイル名としては完成です。

  const files = Object.keys(pkg.data).filter(n =>
    exts.find(ext => n === `${name}${ext}` || n === `${name}/index${ext}`),
  )
  console.log(files)
  if (files.length > 0) {
    return files[0]
  }

  console.log('------------------------')
  console.log(modpath)
  console.log(Object.keys(pkg.data))
  console.log('------------------------')
  throw new Error(`not found ${modpath}`)
}

処理がここまで及ぶ場合は、拡張子無しでファイル名を指定しているか、ディレクトリそのものを指しています。

まず、そこで拡張子を付与したものや、/index + 拡張子を付与したものにヒットするかどうか調べます。

もし複数ヒットすれば、ヒットした一番はじめのファイルを採用します。

※本当はもっと真面目に https://nodejs.org/api/modules.html に従ってファイル名決定をする必要がありそうです。

さてここまでで、モジュール名とファイル名が完全に確定しました。

    requireStack.push([modname, filename])
    console.log('PUSH', [modname, filename])

requireStackmodnamefilenamepushします。

    const code = pkgs[modname].data[filename].toString()
    const module = new Module()
    module._compile(code, filename)

modnamefilenameが確定しているためオンメモリしたファイルからソースコードを取り出し、module._compuleにより実行をします。

    requireStack.pop()
    console.log('R', requireStack.length, name, module.exports)
    return module.exports || null
  })
  return unhack
}

あとは、requireStackpopしてから、module.exportsを返せばモジュールハックの完了です。

まとめ

パッケージレジストリからパッケージ情報とパッケージのtarballを取得できるので、オンメモリでgzip+tarを展開しておく。モジュールハックを使って対象モジュールを指定された場合にはそのモジュールのソースコードを取り出し、module._compileでNode.js環境に応じた処理を実行させてから、module.exportsを取り出す。

これらによりゼロインストールは実現可能です。

ソースコードに関しては、これらを見るといいでしょう。

ただし、このやり方が正しいか?というと検証がまだ全然足りていません。おそらくpackage.jsonscriptspre-installだのpost-installだのをしているもの、ネイティブモジュールなどでは動作しないでしょう。黒魔術めいたコードの場合も動かない可能性は普通に考えられます。

筆者の手元では、uuidv4 npm パッケージのゼロインストール実行はできています。

宣伝

技術書典7にお越しいただいた皆様ありがとうございました。

技術書典7で出した本のPDF電子版を、Pixiv Boothで頒布しております。よろしければどうぞ。

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

ESLint v6.5.0

v6.4.0 | 次 (2019/10/12 JST or 2019/10/26 JST)

ESLint 6.5.0 がリリースされました。
小さな機能追加とバグ修正が行われています。

質問やバグ報告等ありましたら、お気軽にこちらまでお寄せください。

? 日本語 Issue 管理リポジトリ
? 日本語サポート チャット
? 本家リポジトリ
? 本家サポート チャット

? 本体への機能追加

--env-info CLI option

? #12270

ESLint の実行環境を表示する CLI オプションが追加されました。

バグ報告の際にご利用ください。

$ eslint --env-info
Environment Info:

Node version: v12.11.0
npm version: v6.11.3
Local ESLint version: v6.5.0
Global ESLint version: Not found

? 新しいルール

特になし。

? オプションが追加されたルール

use-isnan enforceForSwitchCase

? #12192, #12207

浮動小数点数の仕様により、NaNとの比較は常にfalseになります。これを報告するuse-isnanルールに、switch文も報告するオプションが追加されました。

/*eslint use-isnan: [error, { enforceForSwitchCase: true }]*/

//✘BAD
switch (foo) {
    case NaN:
        bar();
        break;
}

Online demo

✒️ eslint --fix をサポートしたルール

特になし。

⚠️ 非推奨になったルール

特になし。

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

Node.jsでModule hack (require hack)する

Node.jsにはrequireというモジュールを読み込む機能があります。これはNode.js Module APIで提供されている機能で、割と簡単にハックすることができます。よく Module hack とか require hack と呼ばれるものです。

などが有名どころでしょうか。これらのモジュールでModule hackをすると、本来のNode.jsでは動作しないTypeScriptや拡張されたJavaScriptが動作します。

この記事では Module hack のやり方について説明します。

Module hackのやり方

// TypeScriptの場合
export const Module = require('module') as any

まず module を読み込みます。

拡張子に応じたハックをするのは簡単です。

Module._extensions[ext] = function(m: any, filename: string) {
  return m._compile('console.log("hoge")', filename)
}

先ほどから、Module を as anyany アサーションをしたり、今回の関数の第一引数 m もanyで定義していますが、これは、@types/node で定義されている Module 型の定義では、非公開プロパティ、たとえば _extensions_compile にアクセスできないためです。

実例

// test.js
const Module = require('module')

Module._extensions['.js'] = function(m, name) {
  return m._compile('console.log("piyopiyo")', name)
}

require('./hoge')
// hoge.js
console.log('hoge')

これら2つのファイルを同じディレクトリにおいて、node test.jsを実行すると、Module hack をしてなければ本来 hoge と表示されるところが piyopiyo と表示されます。

解説

Module._extensions は、拡張子ごとにファイルの処理をする関数群です。.js のようにドット込みの拡張子を指定します。

ts-node/registerや@babel/registerのように、ソースコードをトランスパイルして実行する場合には、大体定番となるのが、

Module._extensions['.js'] = function(m, name) {
  return m._compile('console.log("piyopiyo")', name)
}

のように最終的にNode.jsが解釈できる状態にまでトランスパイルしたソースコードを、m._compile(code, name) のようにすることです。

補足

Module._extensionsを書き換えると、もどす時に工夫は必要となります。

export const originalExts: { [props: string]: any } = {}

Object.keys(Module._extensions).forEach(ext => {
  originalExts[ext] = Module._extensions[ext]
})

大体はこのように、本来の関数を保存しておきます。

もっとも、このような hack を元に戻す必要がある機会はあまりないでしょう。

モジュール読み込み自体をhackする

ここまでの hack で、拡張子ごとの hack は可能になりましたが、モジュールそのものを hack するためには別の隠しプロパティにアクセスする必要があります。

const Module = require('module')

const originalLoad = Module._load

Module._load = function(name, parent, isMain) {
  if (name === 'hoge') {
    return { hoge: () => console.log('hoge') }
  }
  return originalLoad(name, parent, isMain)
}

const { hoge } = require('hoge')
hoge()

Module._loadを書き換えると、モジュール読み込みそのものにちょっかいを出せます。ただし本来の関数は保存しておいた方が良いため、const originalLoad = Module._loadで保存しておきます。

このコードでは、hogeという名前のモジュールだけhackします。他のモジュールを読み込んだ場合は、従来読み込むべきモジュールが読み込まれます。

任意のソースコードをモジュールとして返す

先ほどのhackでは、ダイレクトに書いたコードをそのまま実行しています。もちろん、それはそれで用途もあるかもしれませんが、拡張子hackのときと同じように任意のコードを実行して、その結果を返したいときもあるでしょう。

const module = new Module()
module._compile(code, filename)
return module.exports

さきほどもでてきた _compileは、Node.jsの実行環境に応じた処理をやってくれるので便利です。同等の処理を自前、たとえばeval Function vm.runInContextなどでやろうとするとかなり面倒です。

注意点

ここに書いた undocumented なものです。これで今後も含めて正しく動作することは保証されていません。また他の黒魔術を使ったソースコードで問題が生じる可能性があります。

pirates というnpmモジュールを使うと、より安全に Module hack を試すことはできます。自前でやるよりはいいかもしれません。

参考: https://github.com/nodejs/node/blob/master/lib/internal/modules/cjs/loader.js

宣伝

技術書典7で出した本のPDF電子版を、Pixiv Boothで頒布しております。よろしければどうぞ。

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

Sequelizeのwhere句でbetweenを使う方法

環境

Node,js + Express + MariaDB
Sequelize V5

若干ハマった

Sequelizeにおいて、オペレータの使用方法は、バージョンによって差異があるようです。
前は、($文字列)のエイリアスを設定することで、オペレータを使用していましたが
V5では非推奨となっていて
現在はSequelize.Opから提供されるSymbolオペレータを使用することが推奨されているようです。

const Sequelize = require("sequelize")
const Op = Sequelize.Op
router.get("/user",(req,res,next)=>
  {
    models.user.findAll(
    {
      where:{birth:{[Op.between]:["2019-09-09","2019-09-10"]}}
    })
  }
)

悲しみ

適当に検索してたら、文字列オペレータを使った方法がかなり出てきて戸惑いました。
リファレンス最強ですね。

Sequelizeリファレンス : https://sequelize.org/master/manual/querying.html

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

ndenv環境時にWebStormでpackage.jsonのタスク実行時「npmパッケージが指定されていない」と怒られる

社内でもSublimeTextおじさんという名の老害として君臨していたのですが、この度JetBrains軍門に下りました。エディタ生活に慣れきっていたので個人的には困ってる困ってないでいえば困ってはいなかったのですが、品質担保という面から自分ひとりだけSublimeおじさんしているわけにもいかなくなったので、丁度いい機会かと思いWebStormを使い始めることにしました。

試しに過去の個人プロジェクトを開いてみたところニョロニョロだらけでげんなりしたのでそっ閉じしました。

前提

  • Mac
  • WebStorm
  • anyenvndenvを使ってNode.jsのバージョン管理をしております。

npmタブからスクリプト実行時に怒られる

WebStormではnpmタブからpackage.jsonに記述したスクリプトをターミナルを経由せずに実行することが出来ます。早速やってみましょう。

23:02 ' ' の実行中にエラーが発生しました: java.util.concurrent.ExecutionException: com.intellij.execution.ExecutionException: Please specify npm or yarn package

えぇ…。

設定を確認してみる

Preference > Node.js および NPMタブを開くと、Nodeインタープリター欄にはきちんとanyenvのパスが入っていましたが、パッケージ・マネージャーの欄は空欄でした。

スクリーンショット 2019-09-28 23.59.05.png

こいつが怪しそうです。

設定しようとしてハマる

三点リーダからパスを指定しようとしたら、ディレクトリ一覧にanyenvが出てきません。先頭にピリオドがついている隠しディレクトリ扱いのためか、表示されないようです。仕方ないので直接パスを指定します。

指定先のパスがわからずハマる

いざパスを指定しようとしたところ、じゃあどこを指定すればいいのか、という問題にぶち当たり1時間近く頭を抱える羽目になりました。ndenvのバージョン変えてもきちんと動くのか非常に不安ではありますが、グローバルにインストールしているNode.jsでインストールされているnpmのインストール先を指定したところ、package.jsonに記述のあるパッケージのバージョンを確認してくれるようになりました。

スクリーンショット 2019-09-29 0.08.57.png

~/.anyenv/envs/ndenv/versions/v10.15.0/lib/node_modules/npm

パスは適時読み替えて、自身のインストール状況に合わせたパスを指定してください。

npmタブからスクリプトを走らせてみる

設定を保存して再びnpmタブに戻り、スクリプトの実行に再チャレンジしてみます。構成の編集ウィンドウが出てきた場合は、パッケージマネージャーが空欄になっていると思いますので、プルダウンを開くとProjectのところに先程登録したnpmのパスが入っていると思いますので、そちらを指定すれば実行出来るかと思います。

終わり。

これバージョン変えるたびに都度変えなきゃいけないのはプロジェクト行き来するケースだと結構面倒だと思うのですが、実はプラグインでそのへんカバーされてるのでは、と8割くらい書き終わってから思いました。

とりあえず色々弄くり倒してみようかと思います。

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