fs

プロミス化

utilモジュールのpromisifyでラップすることでPromise関数化できます。

const {promisify} = require('util');
const {readFile} = require('fs');

const readFilePromise = promisify(readFile);

readFile

あるディレクトリ上にあるファイルやディレクトリ名の一覧を取得します。

fs.readFile('./package.json', 'utf-8', (err, content) => {
  if (err === null) {
    // `Error: EISDIR: illegal operation on a directory` など
  }
  
  console.log(content);
  // {"name": "...", ...}
}

画像などを読み込む時は、utf-8指定はいりません。

fs.readFile('./static/logo.png', (err, content) => {
  // ...
}

readdir

あるディレクトリ上にあるファイルやディレクトリ名の一覧を取得します。

fs.readdir('.', (err, list) => {
  if (err === null) {
    // `Error: ENOENT: no such file or directory` など
  }
  
  console.log(list);
  // [
  //   '.git',
  //   ...
  //   'yarn.lock'
  // ]
}

Express と JWT (JSON Web Token) をできるだけシンプルに使う

依存

まず必要なパッケージをインストールします。

yarn add express express-jwt jsonwebtoken

トークンを生成

次の1行のコマンドで作れます。第2引数は復元に使うので覚えておきます。

node -e "console.log( require('jsonwebtoken').sign({value:'foo'}, 'a') )"
# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2YWx1ZSI6ImZvbyIsImlhdCI6MTUzNTU2NDUyNH0.Mkz75dMlWnK2Y_8g7CGjqwLNvlc1pC3O8znGuUP5ZS8

復元を試します。第2引数は同じものを指定します。
ちゃんとオブジェクトが表示されてれば大丈夫。

node -e "console.log( require('jsonwebtoken').verify('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2YWx1ZSI6ImZvbyIsImlhdCI6MTUzNTU2NDUyNH0.Mkz75dMlWnK2Y_8g7CGjqwLNvlc1pC3O8znGuUP5ZS8', 'a') )"                                                                         02:42:24
# { value: 'foo', iat: 1535564524 }

Expressサーバーで認証する

まずlocalhost:3333okとだけ返す所まで作ります。

const express = require('express');
const jwt = require('express-jwt');
const app = express();
app.use('/', /* jwt middleware が入る */ (req, res) => res.sendStatus(200));
app.listen(3333);

app.useの第2引数でexpress-jwtを使います。設定は次の通り。

  • secretは上記で発行した時に使ったものを
  • requestPropertyは、デフォルトではreq.userに値が入るのですが、今回はユーザー情報でも何でもないのでなんとなくreq.data
    • ちなみにresultPropertyreq.res.dataになるみたいですなんか知った)
  • getTokenはトークンの置き場を自分で探して見つけたトークンを返してます。今はAuthorizationだけ見ます
app.use(
  '/',
  jwt({
    secret: 'a',
    requestProperty: 'data',
    getToken(req) {
      if (
        req.headers.authorization &&
        req.headers.authorization.split(' ')[0] === 'Bearer'
      ) {
        return req.headers.authorization.split(' ')[1];
      }

      return null;
    }
  }),
  (req, res) => res.status(200).send(req.data)
);

実装は終わったで、あとはサーバーを建ててcurlで試してみます。レスポンスがjsonなら大丈夫です!

curl -H 'Authorization: Bearer {トークン}' http://localhost:3333
# {"value":"foo",...}

UnauthorizedError: invalid signatureの場合はどこかおかしいのでsecret合ってるかどうかなど確認しましょう。

Logger パッケージである Signale の使い方

基本

デフォルトタイプ

最初から以下タイプが定義されてるので、足りてるならsignale[TYPE]('メッセージ')の形で即使うだけです。

  • await
  • complete
  • error
  • debug
  • fatal
  • fav
  • info
  • note
  • pause
  • pending
  • star
  • start
  • success
  • warn
  • watch
  • log

以下はCLIでの使用例です。

const signale = require('signale');

const yargs = yargs.xxx(/* ... */);
// 引数情報表示
signale.info(`Port: ${argv.port}`);
// サーバー起動処理
signale.start('Server'));
// App起動処理
signale.start('App');
// ℹ  info      Port:  33322
// ▶  start     Server
// ▶  start     App

カスタムタイプ

SignaleClass をインポートして、自分用に作る必要があります。

const {Signale} = require('signale');

const signale = new Signale({
  types: {
    sake: {
      color: 'green',
      badge: '🍶',
      label: 'sake'
    }
  }
});
signale.sake('Kokuryu');
// 🍶  sake      Kokuryu

補足情報をつける

スコープ、

const appLogger = signale.scope('cli:app');
appLogger.start('listen');
// [cli:app] › ▶  start     listen

や現在のファイル名、

signale.config({displayFilename: false});
// [cli.js] › ✔  success   foo

またdisplayDateで年月日、displayTimestampで時分秒を出したりできます。

ログを表示しないようにする

開発中だけ出したいとかあると思います。
これは設定のstream/dev/nullに書き込むようにするとすべて捨てる事ができます。(以下はdev-nullパッケージを使ってます)

const devnull = require('dev-null');
const signale = new Signale({stream: devnull()});

一部だけ消したい時は各typesで設定する必要があります。例えば、「debugタイプ捨ててぇ」時

const devnull = require('dev-null');
const signale = new Signale({
  types: {
    debug: {
      stream: [devnull()]
    }
  }
});

👦🏻「ログファイルに出してぇ」

const signale = new Signale({
  stream: require('fs').createWriteStream('/tmp/foo.log')
});

GitHub API からあるリポジトリのファイル内容取得

コード

以下はとあるリポジトリのfoo.txtを取得しています。
取得した内はbase64でエンコードされてるのでデコードが必要です。

/**
 * `yarn add dotenv got`
 */

require('dotenv').config();
const got = require('got');

(async () => {
  const {body} = await got(
    'https://api.github.com/repos/:owner/:repo/contents/foo.csv',
    {
      json: true,
      headers: {
        accept: 'application/vnd.github.v3+json',
        authorization: `token ${process.env.TOKEN}`
      }
    }
  );

  const decodedContent = Buffer.from(body.content, 'base64').toString();
  console.log(decodedContent);
})().catch(err => {
  console.log(err);
});

.envにはTOKENを定義しておきます。

TOKEN=xxx

日本語ファイルを取得

ファイルというか URL の日本語部分はパーセントエンコーディングします。テスト.csvを取得したいならこんな感じです。

`https://api.github.com/repos/geekcojp/directory-maps/contents/${encodeURIComponent(
  'テスト'
)}.csv`;

過去の状態のファイルを取得

URLの後ろにref={COMMIT_HASH || BRANCH || TAG}なParamをつけます。

`https://api.github.com/repos/geekcojp/directory-maps/contents/${encodeURIComponent(
  'テスト'
)}.csv?ref=fdbb18abe52965420c9c96b9502ddf14ba28dd7d`;

Github から Personal Access Token の取得

Settings -> 'Developer settings' -> 'Personal access tokens' から 'repository' にチェックを入れて生成

generate token

npm install 時の nodejs バージョンに関するエラーの回避

こういうやつ。

# get-caller-file@2.0.1: The engine "node" is incompatible with this module. Expected version "6.* || 8.* || >= 10.*". Got "9.11.2"
# get-caller-file@2.0.1: The engine "node" is incompatible with this module. Expected version "6.* || 8.* || >= 10.*". Got "9.11.2" error Found incompatible module

Yarn を使っているなら --ignore-engines を付けると、ローカルのNodeJSのバージョンによってエラーになることを回避できます。

yarn --ignore-engines

サイトマップを作る

スキーマ

スキーマ情報はwww.sitemaps.org/protocol.htmlに書かれいます。手書きでxmlを書くのは面倒くさいのでnodejsスクリプトで生成する感じにしてみる。

2つのパッケージをインストール

以下の2つをインストールします。

yarn add xml prettify-xml

xmlは木構造なオブジェクトをxmlにフォーマットしてくれて、prettify-xmlはxmlの整形パッケージです。

サンプルコード

簡単に書いてみました。

const fs = require('fs');
const path = requrie('path');
const xml = require('xml');
const prettifyXml = require('prettify-xml');

const tree = {
  urlset: [
    {_attr: {xmlns: 'http://www.sitemaps.org/schemas/sitemap/0.9'}},
  ]
};

tree.urlset.push({
  url: [
    {loc: 'そのページのURL',
    {lastmod: 'いつ変更したか'},
    {changefreq: '更新頻度'},
    {priority: '優先度 0~1 例えば 0.5'},
  ]
})
// ...他すべてのページを`push`
 
const sitemap = prettifyXml(xml(tree, {declaration: true}));
fs.writeFileSync(path.join(__dirname, '相対パス/sitemap.xml'), sitemap);

tree.urlset.push周りは色々リファクタして手間が少なくするようにするといいですね。
ちなみにchangefreqは以下から適当なのを選びましょう。

  • always
  • hourly
  • daily
  • weekly
  • monthly
  • yearly
  • never

lastmodchangefreqなどはは特に必須情報ではないようです。

node コマンドでランダム色をさくっと取得

node -e "console.log('#' + crypto.randomBytes(3).toString('hex'))"
# #1f514c

ランダム色をさくっと取得してcolorhexa.comで見る

xargsと組み合わせればcolorhexa.comで詳細を見れるようにもできます。その場合色値の前の#はいらないので少し注意です。

node -e "console.log(crypto.randomBytes(3).toString('hex'))" | xargs -I@ open https://www.colorhexa.com/@

使っている node のバージョンが対象外で yarn 時にエラーになる

僕が見たものだとこんなのや

error get-caller-file@2.0.1: The engine "node" is incompatible with this module.
Expected version "6.* || 8.* || >= 10.*".Got "9.11.2"

こんなのです。

error upath@1.0.4: The engine "node" is incompatible with this module.
Expected version ">=4 <=9". Got "10.15.1"

追加(2019-07-03)

sane@4.1.0: The engine "node" is incompatible with this module. Expected version

これは一応yarn--ignore-enginesというオプションをつければ回避できます。

yarn --ignore-engines

node-glob のパターンの書き方

以下のようなファイル構造だとします。

.
├── ba
│   └── z.mdx
├── bar
│   └── b.mdx
├── baz
│   └── c.mdx
└── foo
    └── a.mdx

3 directories, 3 files

0か複数

*を使うと0か複数を表せます。

glob('ba*/**/*.mdx', (_, files) => {
  console.log(files);
});

これは[ 'ba/z.mdx', 'bar/b.mdx', 'baz/c.mdx' ]を得ます。*の部分は無いものでもマッチするのでbaも含まれます。

1か複数

?を使うと1か複数を表せます。

glob('ba?/**/*.mdx', (_, files) => {
  console.log(files);
});

これは[ 'bar/b.mdx', 'baz/c.mdx' ]を得ます。?は必ず何かしらの文字列がある必要があるので今度はbaは含まれません。

範囲

[]で囲むと範囲という意味になります。例えば[a-z]a,b,c,d,...みたいな意味になります。

glob('qqqq/ba[a-z]/**/*.mdx', (_, files) => {
  console.log(files);
});

よってこれも[ 'bar/b.mdx', 'baz/c.mdx' ]を得ます。

完全マッチの複数

@(foo|b?r)と書くとfoob?rの時にマッチします。中で?*など使えるので柔軟に複数指定できます。

glob('qqqq/@(foo|bar)/**/*.mdx', (_, files) => {
  console.log(files);
});

これは[ 'foo/a.mdx', 'bar/b.mdx' ]を得ます。

複数マッチの否定

!(foo|bar)と書くとfoobarじゃないものにマッチします。

glob('qqqq/!(foo|bar)/**/*.mdx', (_, files) => {
  console.log(files);
});

これは[ 'qqqq/baz/c.mdx' ]を得ます。

何でもいいディレクトリ

**を使うとディレクトリの名前・どこにあるかに関係なくマッチするようにできます。

glob('**/*.mdx', (_, files) => {
  console.log(files);
});

これは[ 'foo/a.mdx', 'bar/b.mdx', 'baz/c.mdx' ]を得ます。これはhoge/fuga/piyo/a.mdxというようなファイルであってもマッチします。

base64 への encode / decode

Encode

Buffer で読み込んでからtoStringbase64に変換します。

Buffer.from('foo').toString('base64');
// 'Zm9v'

Buffer で読み込めれば何でも。

class Foo {
  [Symbol.toPrimitive]() {
    return 'foo';
  }
}

Buffer.from(new Foo()).toString('base64');
// 'Zm9v'

Decode

encodebase64として Buffer で読み込み、文字列化します。

Buffer.from('Zm9v', 'base64').toString();
// 'foo'

マルチスレッド: worker_threads

NodeJS 公式提供のworker_threadsモジュールを使うとマルチスレッドで実行されるコードを書けます。

スレッドの作成

Workerクラスから作ります。このコンストラクタには第一引数に別スレッドで実行するjsファイルへのパス、第二引数(オプショナル)でその別スレッドに渡す値が渡せます。

const {Worker} = require('worker_threads');

new Worker('/path/to/ping-pong.js', {
  workerData: {
    value: 'ping'
  }
});

送られてきたデータはworker_threadsworkerDataで受け取れます。このオブジェクトはコンストラクタで渡したworkerDataと同じ構造を持ちます。 例えば/path/to/ping-pong.jsでは以下のworkerData箇所のようにその値を使います。

const {isMainThread, workerData, parentPort} = require('worker_threads');

if (isMainThread) {
  throw new Error("メインスレッドでは実行できません");
}

const {value} = workerData;

if (value === 'ping') {
  parentPort.postMessage({value: 'pong'});
}

/path/to/ping-pong.jsはメインスレッドでの実行を想定してません。isMainThreadboolean値で「メインスレッドでの実行か?」が判断できます。
その気になればこの判断を使って、一つの.jsファイルにメインスレッドの処理と別スレッドの処理の両方を書けます。

あるスレッドからメインスレッドへデータを送り返すにはparentPost.postMessage(data)を使います。これによりメインスレッドのWorker#onmessageイベントのハンドラーでdataを同じ構造で受け取れます。

Worker#onでは他にerrorexitイベントも扱えます。これらはそれぞれ、あるスレッドの中でエラーが投げられた時や、コードが終了させられた場合に呼ばれます。


ここでは大げさにスレッドを分割してみて、

  1. 別スレッドで MarkDown ファイルREADME.mdを取得
  2. 別スレッドで MarkDown から HTML に変換
  3. 結果を表示

のようなプログラムを書いて使い方を見てみます。

README.md を取得

const {
  Worker,
  isMainThread,
  parentPort
} = require("worker_threads");
const path = require("path");
const fs = require("fs");
const { promisify } = require("util");
const readFile = promisify(fs.readFile);

if (isMainThread) {
  /**
   * @returns {Promise<string>} README.md の中身を持つ Promise
   */
  const readReadme = () => {
    return new Promise((resolve, reject) => {
      new Worker(__filename)
        .on("message", ({ contents }) => resolve(contents))
        .on("error", error => reject(error));
    });
  };

  (async () => {
    // ...
    // 略
    // ...
  })();
} else {
  (async () => {
    const contents = await readFile(
      path.resolve(__dirname, "README.md"),
      "utf-8"
    );

    parentPort.postMessage({ contents });
  })();
}

1つのファイルで2つのスレッドの処理内容を書いてます。readReadmeによって別スレッドが建てられ、そこでREADME.mdを読み込み、内容を返してます。
__filenameのように Worker には実行中のファイルと同じ名前のファイル名を渡します。このスクリプトは二度実行されることになりますが、isMainThreadにより処理を切り分けてます。

別スレッドで MarkDown から HTML に変換

今度は別ファイル<project-root>/workers/compile-markdown-to-html.jsで書いてみます。この中身は以下の通りです。ごちゃごちゃしてますが「contentsという MarkDown で書かれたテキストを受け取り、HTML に変換してメインスレッドに返す」ということをやってます。

const {
  isMainThread,
  parentPort,
  workerData
} = require("worker_threads");

if (isMainThread) {
  throw new Error("メインスレッドでは実行できません");
}

(async () => {
  const unified = require("unified");
  const markdown = require("remark-parse");
  const remark2rehype = require("remark-rehype");
  const minify = require("rehype-preset-minify");
  const stringify = require("rehype-stringify");

  const { contents } = workerData;

  unified()
    .use(markdown)
    .use(remark2rehype)
    .use(minify)
    .use(stringify)
    .process(contents, (err, file) => {
      if (err !== null) {
        throw err;
      }

      parentPort.postMessage({ file });
    });
})();

メインスレッド側に以下のコードを追記します。compileMarkDownToHTMLは、compile-markdown-to-html.jsから別スレッドで MarkDown なテキストと共に建て、変換が終わったら結果を返す非同期関数です。

/**
 * @returns {Promise<string>} HTML を持つ Promise
 */
const compileMarkDownToHTML = contents => {
  return new Promise((resolve, reject) => {
    new Worker(
      path.resolve(
        __dirname,
        "workers/compile-markdown-to-html.js"
      ),
      {
        workerData: {
          contents
        }
      }
    )
      .on("message", ({ file }) => resolve(file.contents))
      .on("error", error => reject(error))
  });
};

結果を表示

では今まで作ったreadReadmecompileMarkDownToHTMLを使って変換後結果を表示します。

(async () => {
  const readmeContents = await readReadme();
  const html = await compileMarkDownToHTML(readmeContents);

  console.log(html);
})();

リポジトリ

上記のコードはリポジトリからgit cloneyarn && node main.jsで実際に走らせれます。

エラー色々

Electron ビルド時

環境

  • MacOS Mojave

ログ

Unhandled rejection Error: Could not find "wine" on your system.

Wine is required to use the appCopyright, appVersion, buildVersion, icon, and
win32metadata parameters for Windows targets.

Make sure that the "wine" executable is in your PATH.

See https://github.com/electron-userland/electron-packager#building-windows-apps-from-non-windows-platforms for details.
    at Process.ChildProcess._handle.onexit (internal/child_process.js:232:19)
    at onErrorNT (internal/child_process.js:407:16)

直す

文にあるように、以下の手順でwineをインストールします。

# Homebrew をインストール
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

brew cask install xquartz
# ...
# ==> Downloading https://dl.bintray.com/xquartz/downloads/XQuartz-2.7.11.dmg
# ...
# 🍺  xquartz was successfully installed!

brew install wine
# ...
# ==> Downloading https://homebrew.bintray.com/bottles/wine-3.0.4.sierra.bottle.tar.gz
# ...
# ==> Caveats
# You may also want winetricks:
#   brew install winetricks
# ==> Summary
# 🍺  /usr/local/Cellar/wine/3.0.4: 8,704 files, 633.5MB

以下のようにダイアログが建つかもなので、インストールしてください。

Image from Gyazo

NODE_MODULE_VERSION 57. This version of Node.js requires NODE_MODULE_VERSION 59. Please try re-compiling or re-installing

rm -rf node_modulesして削除後、再度yarnでインストール

bcrypt 周りの場合

例えば以下のようなパターンの場合は、

Error: The module .../node_modules/bcrypt/lib/binding/bcrypt_11b.node
was compiled against a different Node.js version using
NODE_MODULE_VERSION 59. This version of Node.js requires
NODE_MODULE_VERSION 64. Please try re-compiling or re-installing

以下で治りました。

npm rebuild bcrypt --update-binary