PostCSS + cssnext と CSS Modules を試す

2017年12月27日 2 開発 , , ,

このあいだ書いた記事の締めで予告したとおり Stylus からの移行先として PostCSS + cssnext を試す。

2017/12/29 追記 : cssnext は CSS.next と呼べるか?

本記事のコメント欄にて @mysticateaさん から指摘され、cssnext の機能すべてを CSS.next と呼ぶのは妥当ではないと判断した。

私が CSS.next だと思っていた CSS Nesting Module は CSS Working Group (以下、csswg) の draft には存在せず上記 issue では cssnext が次世代の規格と宣伝してるけど正式な draft としては提出されてないと書かれてる。

これから提出される可能性はあるものの、現時点の cssnext は csswg draft に完全準拠してるわけではない。CSS.next な機能とそうでないものが混在してる。

とはいえ PostCSS の CSS 構文拡張をプラグイン化するアイディアと cssnext がそこから Babel 的に .next なものを選定する方針は支持したい。よって年明けになるが cssnext 側へ

  • csswg draft とそのステータスを反映するようにしてほしい
  • csswg draft に挙げられているものだけ有効にするモードを追加してほしい
    • csswg draft のステータスがパラメーターになるような感じ
    • 標準化されたものだけ有効とか
    • モード追加としたのは現行 cssnext ユーザー環境を壊さないため

という感じの要望を出す予定。

PostCSS + cssnext

PostCSS は AltCSS の一種となるツール。Stylus や Sass はそれ自体をひとつの言語としているが PostCSS は JavaScript における Babel のようななもので、本体はランタイムに過ぎない。様々なプラグインを組み合わせて好みの変換環境を構築してゆく。

しかし個々のプラグインを調査するのはコストがかかる。また Babel でいう babel-preset-env のように将来の CSS 仕様 (以下、CSS.next) へなるべく準拠して書きたいところ。という要望を叶えるものとして cssnext がある。

cssnext は CSS.next な記法を実現するための PostCSS プラグイン集。以下の記事は対応する仕様と照らし合わせながら解説しており参考になる。

難点として babel-preset-env と異なり、変換対象となる環境を指定できない。たとえば Chrome だけサポートした CSS.next 記法があるとする。そして Web ブラウザーとしても Chrome だけサポートするからそこは変換したくない。このケースだと babel-preset-env では環境の下限を Chrome にすることで無駄な変換を避けられるのだが cssnext にはこの機能がない。

babel-preset-envECMAScript 6 compatibility table に準拠して環境を特定するのだが CSS にはこうした網羅的な環境基準がないため難しいのだろう。現時点では常に CSS3 相当の変換であることを意識する必要あり。

PostCSS CLI による単体利用

PostCSS + cssnext を利用する方法として webpack 前提の記事が散見される。しかし PostCSS は単体で利用可能。いきなり webpack と組み合わせると設定が一気に複雑化して混乱するだけなので、まずは単体で試してみる。

プロジェクト構成

プロジェクト構成は以下。

.
├── package.json
├── postcss.config.js
└── src
    ├── assets
    │   └── index.html
    └── css
        ├── App.css
        ├── Base.css
        ├── Content.css
        ├── Footer.css
        ├── Header.css
        └── Variables.css

src/css へ CSS を格納。これを開発版なら src/assets、リリース版は dist/ に変換して bundle.css というファイル名で出力する。開発版では Source Maps にも対応。

npm

必要な npm をインストール。

$ npm i -D postcss-cli postcss-cssnext postcss-import cssnano

それぞれの役割をまとめる。

npm 役割
postcss-cli PostCSS の CLI 兼、ランタイム。
postcss-cssnext cssnext 用 PostCSS プラグイン集。CSS.next な記法を解釈して変換する。
postcss-import CSS 内の @import を解釈して複数ファイルを結合するための PostCSS プラグイン。CSS.next の範疇から外れるが、便利なので追加。
cssnano CSS を minify するためのツール。プラグインではないが PostCSS の README で minify 用として紹介されているため採用。

postcss.config.js と npm-scripts

PostCSS 用の設定ファイルを定義。ファイル名を postcss.config.js にすると自動認識してくれる。内容は PostCSS よりも postcss-cli の README のほうが詳細に解説してあるので一読を。

module.exports = (ctx) => {
  const PROD = (ctx.env === 'prod')

  return {
    map: PROD ? null : { inline: false },
    plugins: {
      'postcss-import': {},
      'postcss-cssnext': {},
      'cssnano': PROD ? { autoprefixer: false } : false
    }
  }
}

設計思想や記法は webpack っぽい。Object または Functionexport する。Function だと CLI 引数を判定できる。これを利用して package.json の npm-scripts から開発版とリリース版を切り替えている。

{
  "scripts": {
    "build": "postcss ./src/css/App.css -o ./src/assets/bundle.css",
    "watch:css": "postcss ./src/css/App.css -o ./src/assets/bundle.css -w",
    "release:css": "postcss ./src/css/App.css -o ./dist/bundle.css -e prod"
  }
}

CLI の第一引数が CSS のエントリー ポイントになる。postcss-import を追加したので、このファイルから @import をたどって参照が解決される。-o が出力指定。-w でファイル監視 & 変更時の自動変換、-e はその後に指定した文字列を postcss.config.jsFunction に渡してくれる。これは任意だが NODE_ENV で用いられる production に基づき、それを省略した prod とした。

CSS.next + @import で書いてみる。

環境の準備が整ったので CSS.next ならではの機能と @import を使用した CSS を書いてみる。まずはエントリー ポイントになる App.css

@import "./Base.css";
@import "./Header.css";
@import "./Content.css";
@import "./Footer.css";

各 CSS のインポートのみを定義。エントリー ポイント以上のことはしない。こうすると何か問題が起きたとき、読み込み順と CSS 定義のどちらが原因であるかを切り分けやすくなる。個々の CSS が双方にインポートしあわない限り、順番を制御しているのはエントリー ポイントのみとなる。

次に各 CSS で共通使用する変数を定義。名前は Variables.css としておく。今回は色だけだが将来はサイズなども定義することになるだろう。それを踏まえて Colors ではなく Variables とした。

:root {
  --white: #fbfcfa;
  --turquoise: #1abc9c;
  --green_sea: #16a085;
  --emerald: #2ecc71;
  --nephritis: #27ae60;
  --peter_river: #3498db;
  --belize_hole: #2980b9;
  --amethyst: #9b59b6;
  --wisteria: #8e44ad;
  --wet_asphalt: #34495e;
  --midnight_blue: #2c3e50;
  --sun_flower: #f1c40f;
  --orange: #f39c12;
  --carrot: #e67e22;
  --pumpkin: #d35400;
  --alizarin: #e74c3c;
  --pomegranate: #c0392b;
  --clouds: #ecf0f1;
  --silver: #bdc3c7;
  --concrete: #95a5a6;
  --asbestos: #7f8c8d;
}

CSS 変数については MDN の記事がわかりやすい。色の定義と名前は本ブログや自作 Redmine テーマでも使用している Flat UI からもってきた。

これを読み込みつつ、CSS.next の CSS Nesting Module Level 3 を使用した Header.css を定義。

@import "Variables.css";

.header {
  background-color: var(--alizarin);

  & ul {
    margin: 0;
    padding: 0;
  }

  & li {
    text-align: center;
    line-height: 2.5em;
    display: inline-block;

    & a {
      margin: 0;
      width: 10em;
      height: 2.5em;
      color: var(--white);
      border-top: solid 2px var(--alizarin);
      box-sizing: border-box;
      display: inline-block;
      text-decoration: none;

      &:hover {
        border-top: solid 2px var(--sun_flower);
      }
    }
  }
}

Variables.css は他の CSS からも読み込むのだが、結合しても同じものが多重定義されることはないので安心。CSS Nesting Module は Stylus や Sass などでお馴染みの機能。& が直上の Selector を指している。そのため .header 直下で & ul と書けば .header ula に対して &:hover なら a:hover に変換される。これを利用して BEM を簡潔に書くことも可能。

  • 2017/12/29 追記
    • CSS Nesting Module は 2017/12/29 現在、CSS Working Gropu draft には挙げられていません
    • つまり CSS.next の機能とは呼べません
    • cssnext という AltCSS の独自機能となります
    • 元の表現は残しますが、これを CSS.next とする誤解が私以外にも広まるのは好ましくないため、この注記にて補足することにしました

Source Maps と minify

Source Maps を有効にするなら map オプションを指定する。CSS 埋め込みではなく単体ファイルとして出力したいなら

{
  map: PROD ? null : { inline: false }
}

のように inline: false とすればよい。このオプションが無効値だと Source Maps を出力しなくなるのでリリース版ではそうしている。minify は少々、特別で cssnano をインストールして plugins に指定することで実行される。これもリリース版のみとした。

{
  plugins: {
    'postcss-import': {},
    'postcss-cssnext': {},
    'cssnano': PROD ? { autoprefixer: false } : false
  }  
}

cssnano のオプション指定で autoprefixer を無効化している。これは cssnext 側に含まれており、無効化しないと変換時に冗長であると警告される。

Processing src/css/App.cssWarning: postcss-cssnext found a duplicate plugin ('autoprefixer') in your postcss plugins. This might be inefficient. You should remove 'autoprefixer' from your postcss plugin list since it's already included by postcss-cssnext.
Note: If, for a really specific reason, postcss-cssnext warnings are irrelevant for your use case, and you really know what you are doing, you can disable this warnings by setting  'warnForDuplicates' option of postcss-cssnext to 'false'.

この警告は postcss-cssnext のオプションに warnForDuplicates: false を指定することでも無効化できる。しかし本当に警告を表示すべき場合も無効化されてしまう。本来は警告メッセージを読んで個別に対応してゆくべきなのでそうした。

変換してみる

npm-scripts に定義されたコマンドを実行して CSS.next なファイル群をひとつの CSS に変換してみる。コマンドとしては

コマンド 役割
build 開発用。Source Maps あり。単体で変換結果をチェックする場合に使用する。
watch 開発用。Source Maps あり。ファイルを監視して変更を検知したら CSS を自動変換する。browser-sync による Web ブラウザー自動表示も設定している。開発時は通常、こちらを使用する。
release リリース用。Source Maps なし、minify あり。

となるので、それぞれ

$ npm run build
$ npm run watch
$ npm run release

とすればよい。出力されたファイルを見れば適切な変換がおこなわれていることを確認できるはず。

webpack (CSS Modules)

PostCSS + cssnext を webpack で利用することも可能だが、単に CSS を変換するだけなら PostCSS CLI のほうが webpack 依存を避けられて好ましい。どうせ webpack を使うなら領発揮となる CSS Modules を採用してみよう。ということで React + CSS Modules 構成を試す。

React における CSS

Web Components を指向してるライブラリーやフレームワークでは HTML/CSS/JS をどのように組み合わせるかが課題になる。React の場合、HTML/JS は Virtual DOM と JSX (option) で関連付けるのだが CSS については

  • CSS 単体
    • 普通に CSS を定義してそれと関連付ける
    • React コンポーネント側からは className などで CSS クラス名を明示する
    • React コンポーネントの出力も含めた最終的な HTML を想定しながら CSS を定義
    • CSS クラス名の衝突回避は BEM などの命名規則でおこなう
  • CSS in JS
    • React: CSS in JS // Speaker Deck
    • JavaScript の Object として CSS を定義して React コンポーネントと関連付ける
    • CSS は HTML 要素の style 属性に出力される
  • CSS Modules
    • css-modules/css-modules
    • CSSモジュール ― 明るい未来へようこそ | プログラミング | POSTD
    • CSS を JavaScript のモジュールとして扱う
    • CSS は普通に定義、それを JavaScript 側で import する
    • import されたスタイルを React コンポーネントの className に指定することで関連付け
    • CSS はファイルとして出力される
    • CSS ファイル内のクラス名は React コンポーネントとクラス名など (明示的に変更も可能) を元にしたハッシュになる
    • CSS を React コンポーネント単位で分割していれば、この命名規則により自動的に衝突は回避される

がある。私はこれまで CSS 単体を利用してきた。React や webpack などに依存せず、通常の CSS 開発スタイルだけで独立して運用可能なのがその理由。

かつて CSS in JS を検討した際は style 属性に出力されるため CSS ファイルのキャッシュにのらないのと style 属性の制限により疑似要素、擬似クラスが利用できない点を気にして見送った。CSS Modules はこれらの弱点を克服しているのだが webpack など特定のツールに強く依存する点が受け入れられなかった。

しかし Web Components 指向として HTML/CSS/JS をセットであつかうなら、ファイルとしても併置してあるほうが管理しやすい。例えば Header というコンポーネントなら

Header.css
Header.js
Header.test.js

のように構成されていてほしい。

CSS 単体でも Stylus や postcss-import で参照解決すれば、同等の構成を実現可能ではある。しかし CSS エントリー ポイントとの関係を意識しなければならないし、併置しても React コンポーネントとの関連付けは CSS クラス名になるので疎結合で設計的に半端だし、とイマイチだ。

ならばいっそ CSS Modules に舵を切るほうがよいのでは?といことでそうしてみる。

プロジェクト構成

プロジェクト構成は以下。

.
├── package.json
├── postcss.config.js
├── webpack.config.babel.js
└── src
    ├── assets
    │   └── index.html
    └── js
        ├── App.js
        └── components
            ├── App.js
            ├── Content.css
            ├── Content.js
            ├── Footer.css
            ├── Footer.js
            ├── Header.css
            ├── Header.js
            └── Variables.css

ファイル出力の仕様については PostCSS CLI 版と同じ。webpack 設定が追加されて CSS が React コンポーネントに併置しているのが大きな特徴。

npm

必要な npm をインストール。webpack と CSS 関連だけ。PostCSS CLI で説明したものと Babel 系は割愛。

$ npm i -D webpack css-loader postcss-loader extract-text-webpack-plugin

それぞれの役割をまとめる。

npm 役割
webpack Bundler。JavaScript や CSS の参照を解決、指定されたファイル単位にまとめて出力する。
css-loader webpack loader。CSS の参照を解決してくれる。
postcss-loader webpack loader。PostCSS に基づく CSS 変換を担当。
extract-text-webpack-plugin webpack plugin。css-loaderpostcss-loader の出力を受けて CSS ファイル化する。

postcss.config.js

PostCSS と webpack は Source Maps 出力など機能的に被るところがある。そのため主従をしっかり決めて一方へ寄せるのが大切。今回は PostCSS を従として必要最小の設定にとどめる。そのため postcss.config.js の定義は実にシンプル。

module.exports = {
  plugins: {
    'postcss-import': {},
    'postcss-cssnext': {}
  }
}

PostCSS は CSS.next 変換のみに徹し、Source Maps と minify の指定は webpack 側でおこなう。

webpack.config.babel.js

webpack の設定は以下。JavaScript 分も含まれているため長いが、すべて掲載してから CSS 関連を解説する。

import WebPack from 'webpack'
import MinifyPlugin from 'babel-minify-webpack-plugin'
import ExtractTextPlugin from 'extract-text-webpack-plugin'

export default (env) => {
  const PROD = !!(env && env.prod)

  return {
    entry: './src/js/App.js',
    output: {
      path: PROD ? `${__dirname}/dist` : `${__dirname}/src/assets`,
      filename: 'bundle.js',
      publicPath: '/'
    },
    devtool: PROD ? '' : 'source-map',
    module: {
      rules: [
        {
          test: /\.js$/,
          exclude: /node_modules/,
          use: {
            loader: 'babel-loader'
          }
        },
        {
          test: /\.css$/,
          use: ExtractTextPlugin.extract([
            {
              loader: 'css-loader',
              options: {
                modules: true,
                importLoaders: 1,
                sourceMap: !(PROD),
                minimize: PROD ? { autoprefixer: false } : false
              }
            },
            'postcss-loader'
          ])
        }
      ]
    },
    devServer: {
      contentBase: './src/assets'
    },
    plugins: PROD ? [
      new WebPack.DefinePlugin({
        'process.env.NODE_ENV': JSON.stringify('production')
      }),
      new MinifyPlugin({
        replace: {
          'replacements': [
            {
              'identifierName': 'DEBUG',
              'replacement': {
                'type': 'numericLiteral',
                'value': 0
              }
            }
          ]
        }
      }, {}),
      new ExtractTextPlugin({ filename: 'bundle.css' })
    ] : [
      // development
      new ExtractTextPlugin({ filename: 'bundle.css' })
    ]
  }
}

今回の構成では CSS の参照と出力について JavaScript 側の設定に影響される。そもそも CSS Modules を採用した時点で設計的にも JavaScript と密結合になるわけだから、そのようなものと受け入れよう。これを前提として CSS 関連だけ整形抜粋。

{
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ExtractTextPlugin.extract([
          {
            loader: 'css-loader',
            options: {
              modules: true,
              importLoaders: 1,
              sourceMap: !(PROD),
              minimize: PROD ? { autoprefixer: false } : false
            }
          },
          'postcss-loader'
        ])
      }
    ]
  },
  plugins: [
    new ExtractTextPlugin({ filename: 'bundle.css' })
  ]
}

extract-text-webpack-plugin が重要な働きをする。module.rules では .css ファイルに対して extract メソッドを呼び出している。これは既存の loader を利用して参照解決や変換を実行するためのもの。引数の型が何通りかあるのだけど今回は Array で loader を列挙する方式を採用。順番としては css-loader に参照を解決させてから postcss-loader で CSS 変換という流れになる。

CSS Moduels を利用する場合は css-loader のオプションに modules: true, importLoaders: 1 を指定する。PostCSS CLI でおこなっていた cssnano による minify もこの loader 内で実行される。minimizetrue を指定すると有効、Object の場合は有効かつ cssnano へ渡すオプションになる。

plugins で出力する CSS ファイルを指定。ディレクトリーは JavaScript 側と同じ場所になる。

extract-text-webpack-plugin の README にも解説されているが、複数の CSS ファイルを出力したければ newextract-text-webpack-plugin インスタンスを複数生成し、個別に extract メソッドを呼んだうえで plugins にこれらを列挙指定すればよい。Bootstrap のように巨大な CSS フレームワークとアプリ独自部分をわけるときなどに便利な機能である。

CSS と React コンポーネントの関連付け

まずは CSS を定義。Header.css とする。

.header {
  & ul {
  }

  & li {
    & a {
      &:hover {
      }
    }
  }
}

cssnext によりネストも処理できる。これを同じディレクトリーに併置された React コンポーネント Header.js から読み込んで関連付ける。

import React from 'react'
import Styles from './Header.css'

const Item = ({label, url}) => {
  return (
    <li>
      <a href={url}>{label}</a>
    </li>
  )
}

const Header = (props) => {
  const items = props.items.map((item, index) => {
    return <Item key={index} label={item.label} url={item.url} />
  })

  return (
    <nav className={Styles.header}>
      <ul>
        {items}
      </ul>
    </nav>
  )
}

読み込まれた Object には CSS 側のクラス名をもつプロパティーが定義されているので Styles.header のように参照してコンポーネントの className へ指定する。変換された CSS は以下のようになる。

._1g566qHWQaY0sftP77Euo- {
}

._1g566qHWQaY0sftP77Euo- ul {
}

._1g566qHWQaY0sftP77Euo- li {
}

._1g566qHWQaY0sftP77Euo- li a {
}

._1g566qHWQaY0sftP77Euo- li a:hover {
}

クラス名がハッシュになっている。長いので掲載しないが JavaScript 側の変換結果でもこのハッシュが className と関連付けられていることを確認できた。なおハッシュになるのはクラス名だけで要素名などは維持される。

CSS Modules を利用する時点でモジュール単位にスコープが作られるようなものだから、クラスをネストする意味は薄い。例えば以下のようにネストしたなら

.footer {
  & .copyright {
  }
}

変換結果は

._3Krd9752YE1I4ymIQ2tziV {
}

._3Krd9752YE1I4ymIQ2tziV ._32zVUKNOjqIf8Jr8ZonGQT {
}

となるため React コンポーネント側でも

import React from 'react'
import Styles from './Footer.css'

const Footer = (props) => {
  return (
    <footer className={Styles.footer}>
      <p className={Styles.footer + ' ' + Styles.copyright}>Copyright © {props.copyright}</p>
    </footer>
  )
}

という感じでわざわざスペースを挟んで結合しなければならない。設計としてもひとつのモジュールに同じクラス名が重複するのは好ましくないため、クラスはすべてルートに定義するほうがよいだろう。前述の li ならば可変長であることは自明でわざわざクラスをつける意義が薄いから、ネストして要素名で定義する。という感じで使い分けてゆこうと考えている。

変換してみる

変換用の npm-scripts コマンドは PostCSS CLI 版と同じなので割愛。

テキスト エディター

PostCSS + cssnext で CSS を書く際、テキスト エディターの補助があると生産性が向上する。Atom なら

を入れるとよい。language-postcss が PostCSS の構文強調、pigments は CSS 内の color 系を実際の色として強調表示してくれる。これらの組み合わせなのか pigments のパワーなのか分からないが CSS 変数に定義された色も参照先でちゃんと表示してくれる。なにげにすごい。

vscode の場合は

のいずれかを選ぶ。PostCSS syntax は名前のとおり PostCSS の構文強調。

postcss-sugarss-language は前者をベースにしてインテリセンスなどを追加したもの。ただしこちらは標準だと .css を対象としないため vscode のユーザー設定に

{
  "files.associations": {
    "*.css": "postcss"
  }
}

を追加する必要あり。以降は .css でも反応してくれる。vscode-icons を使用していれば vscode のエクスプローラー上でも .css が PostCSS アイコンに変わるので目安になる。

なお残念なことに vscode だと Atom のように CSS 変数の色は強調されない。Color Highlight が Atom でいう pigments かもと期待して入れてみたが CSS 変数には対応しておらず、たまに描画が崩れることもあったのでやめた。これは諦めるしかないようだ。将来に期待している。

まとめ

AltCSS としての PostCSS + cssnext と、それを CSS Modules に組み合わせる方法を試した。

Stylus なら同等の機能を単体で実現可能だが CSS.next 準拠の点で PostCSS + cssnext のほうが好ましい。標準的な記法を採用しておけばツールを捨てたり乗り換えるものが容易になる。Stylus が CSS.next 準拠してくれればよいのだけど、開発の熱は下がってるようだし難しいだろう。

CSS Modules は導入こそ面倒だが Web Components として思い描いてる構成像に近くて運用しやすそう。私はユニット テストもコードと同じディレクトリに併置しているのだが、関係するものを近くに集約すると実に便利である。密結合だからおのずとそれらを往復することが多くて、ならば近いほうがよい。CLI であれ GUI のツリー ビューであれ、近ければ往復しやすくなる。

CSS Modules を採用した場合、ページ全体とコンポーネントの CSS をどう分割するかが課題となる。今回のサンプルだと body, html のスタイルを最初に読み込まれるコンポーネントの CSS に定義したのだが、これは入出力ともにわけたほうがよいのかもしれない。

グラフィック デザイナーとの協業については一考の余地あり。

JavaScript と CSS が併置されていることに抵抗を感じるかもしれない。これまで CSS だけ注視していればよかったのだが、側に自分がいじらなさそうなファイルが置いてあるというのはストレスになるだろう。これは慣れてもらうしかないか。あと React なら SFC (Stateless Functional Components) にしておくとほぼ HTML 相当に単純化されるからグラフィック デザイナーの忌避感が減りそう。これはグラフィック デザイナーの領分についての話でもある。

グラフィック デザイナーから HTML/CSS の断片を提供してもらい、それを SFC + CSS Modules のセットにするのもよい。Sketch ならそういう書き出しも可能だしパーツ分解しやすそうだから、無理に CSS を定義してもらうより効率的かもしれない。断片として管理することを前提とした作業スタイルについて考えたい。

Electron を試す 11 – webpack によるビルド

2017年12月20日 0 開発 ,

これまで Web フロントエンドや Electron の JavaScript ビルドには Browserify と Babel を使用してきた。しかし Browserify の開発は停滞している。現在は個人から org 運営へ移管しており browserify/discuss にて開発者の募集や議論もおこなわれているものの、往時の勢いはない。

私としては CLI のみで完結して設定を package.json に集約しやすい点から Bundler の中では Browserify 推しだった。しかし今後のことを考えると webpack に移行したほうがよいと判断せざるをえない。

というわけで Electron プロジェクトの開発環境に webpack を導入してみた。以下はその覚書。

Browserify + Babel によるビルド

移行前に Browserify + Babel でおこなっていたビルド設定は以下。package.json 内で完結している。

{
  "babel": {
    "presets": [
      ["env", {"targets": {"electron": "1.7"}}],
      "react"
    ],
    "env": {
      "development": {
        "presets": [
          "power-assert"
        ]
      }
    }
  },
  "scripts": {
    "build:js-main": "browserify -t [ babelify ] ./src/js/main/Main.js --exclude electron --im --no-detect-globals --node -d | exorcist --base ./src/assets ./src/assets/main.js.map > ./src/assets/main.js",
    "build:js-renderer": "browserify -t [ babelify ] ./src/js/renderer/App.js --exclude electron -d | exorcist --base ./src/assets ./src/assets/renderer.js.map > ./src/assets/renderer.js",

    "watch:js-main": "watchify -v -t [ babelify ] ./src/js/main/Main.js --exclude electron --im --no-detect-globals --node -o \"exorcist --base ./src/assets ./src/assets/main.js.map > ./src/assets/main.js\" -d",
    "watch:js-renderer": "watchify -v -t [ babelify ] ./src/js/renderer/App.js --exclude electron -o \"exorcist --base ./src/assets ./src/assets/renderer.js.map > ./src/assets/renderer.js\" -d",

    "release:js-main": "cross-env NODE_ENV=production browserify -t [ babelify ] ./src/js/main/Main.js --exclude electron --im --no-detect-globals --node | uglifyjs -c warnings=false -m -d DEBUG=false > ./dist/src/assets/main.js",
    "release:js-renderer": "cross-env NODE_ENV=production browserify -t [ babelify ] ./src/js/renderer/App.js --exclude electron | uglifyjs -c warnings=false -m -d DEBUG=false > ./dist/src/assets/renderer.js"
  }
}

Main/Renderer プロセスで個別に Bundle。目的ごとに接頭辞で分類しており、それぞれ

  • build: 開発用ビルド
  • watch: 開発用ビルド、ファイル監視して更新検知したら差分ビルドする
  • release: リリース用ビルド

となる。かなり長くて複雑なオプションを指定しているが、これは Electron や Node のランタイム モジュールを除外するなどの処理が必要なため。詳細は

を参照のこと。webpack では同等かそれ以上の Bundle 処理を目指す。

webpack

まずは必要な npm のインストール。

$ npm i -D webpack babel-loader babel-minify-webpack-plugin
$ npm i -D babel-core babel-preset-env babel-preset-react

それぞれの役割をまとめる。

npm 役割
webpack Bundler。複数の JavaScript を設定に応じて結合する。
babel-loader webpack から Babel を呼び出して Transpile するためのモジュール。
babel-minify-webpack-plugin minify 用モジュール。webpack 的には uglifyjs-webpack-plugin のほうが一般的で標準プラグインでもあるのだが、Bundle 以外の処理は Babel ファミリーで揃えたかったのでこちらを採用。
babel-core Transpiler となる Babel の本体。
babel-preset-env ES.next で実装されたコードを指定された環境用に変換するためのモジュール。例えば Electron v1.7 向けにすると Electron 上でで有効なもの (ES2015 Classes など) はそのままに、不足しているものだけ代替コードに変換してくれる。
babel-preset-react React 関連 (JSX など) を変換してくれるモジュール。

Babel は Bundler から独立しているため、Browserify 時代の設定をそのまま流用可能。

webpack.config.babel.js

webpack の設定は webpack.config.js というファイルに JavaScript として実装する。この処理を ES2015 以降へ対応させたい場合は Babel をインストールしたうえで webpack.config.babel.js というファイル名にする。今回はこちらでゆく。

import WebPack from 'webpack'
import MinifyPlugin from 'babel-minify-webpack-plugin'

export default (env) => {
  const MAIN = env && env.main
  const PROD = env && env.prod

  return {
    target: MAIN ? 'electron-main' : 'web',
    entry: MAIN ? './src/js/main/App.js' : './src/js/renderer/App.js',
    output: {
      path: PROD ? `${__dirname}/dist/src/assets` : `${__dirname}/src/assets`,
      filename: MAIN ? 'main.js' : 'renderer.js'
    },
    devtool: PROD ? '' : 'source-map',
    node: {
      __dirname: false,
      __filename: false
    },
    module: {
      rules: [
        {
          test: /\.js$/,
          exclude: /node_modules/,
          use: {
            loader: 'babel-loader'
          }
        }
      ]
    },
    plugins: PROD ? [
      new MinifyPlugin({
        replace: {
          'replacements': [
            {
              'identifierName': 'DEBUG',
              'replacement': {
                'type': 'numericLiteral',
                'value': 0
              }
            }
          ]
        }
      }, {}),
      new WebPack.DefinePlugin({
        'process.env.NODE_ENV': JSON.stringify('production')
      })
    ] : [
      // development
    ]
  }
}

内容について解説する。

エントリー ポイントを export default (env) => {...} としているが、これは Environment Variables を使用するためのもの。

Electron は少なくとも Main/Renderer の 2 種類をビルドする必要がある。それらを更に開発用とリリース用にわけるので単純に設定ファイルを定義すると 4 種類もの分岐が発生するうえ、設定や処理の大半は共通。

そのため設定ファイルは共通で外部引数によって処理を分岐する方式を採用した。Environment Variables はこの用途にうってつけの機能である。webpack を CLI から実行する際に

$ webpack --env.prod --env.main

のように --env.XXXX 形式の引数を与えると webpack のエントリー ポイントにした関数の第一引数に展開される。値名だけだと Boolean で true となり --env.XXXX=1 のように明示的に内容を指定することも可能。これを利用して npm-scripts 側は

{
  "scripts": {
    "build:js-main": "webpack --env.main",
    "build:js-renderer": "webpack",
    "watch:js-main": "webpack --env.main --watch",
    "watch:js-renderer": "webpack --watch",
    "release:js-main": "webpack --env.prod --env.main",
    "release:js-renderer": "webpack --env.prod"
  }
}

となる。--env.main により Main/Renderer、--env.prod でリリースと開発版を分岐している。これを受けて webpack 側は

export default (env) => {
  const MAIN = env && env.main
  const PROD = env && env.prod
}

とする。env を直に使わず別の変数に格納している理由は --env.XXXX が未指定だと undefined になるため env.XXXX を判定できないから。参照部分ごとに env の存在をチェックするのは冗長なので事前チェックして変数化しておく。

__dirname__filename

コード中に __dirname__filename が含まれていた場合、標準では webpack がコンテキストにあわせて解決しようとする。これは Node として実行される Main プロセスにおいて問題になる。例えば BrowserWindow.loadURL のパスに指定すると正しくファイルを読み込めない。そのため無効化して Node として処理されるようにする。

{
  node: {
    __dirname: false,
    __filename: false
  }
}

Electron だと __filename を使用することはなさそうだが、いざ参照したくなったときに Node と異なる動作をすると厄介なのでついでに設定しておく。これで開発版だけでなくリソースを asar にパッケージ化したリリース版イメージでも想定どおりパスが処理される。Renderer ではそもそも __dirname などを参照することはないが、使用しないものについて webpack 設定を分岐する意味はないため Main/Renderer 共通の設定としている。

target

webpack の Target 設定はビルド対象の仕向けを指定することで import/require を適切に処理してくれる。Electron 向けとしては

Option Description
electron-main Compile for Electron for main process.
electron-renderer Compile for Electron for renderer process, providing a target using JsonpTemplatePlugin, FunctionModulePlugin for browser environments and NodeTargetPlugin and ExternalsPlugin for CommonJS and Electron built-in modules.

の 2 種類をサポートしている。Main プロセスをビルドする場合、そのまま Node/Electron のモジュールを Bundle すると参照エラーになる。よって electron-main を指定して実行時に参照解決されるようにする。

Renderer プロセスについては electron-rendererweb を指定する。前者だと webpack プラグインと組み合わせることで Node/Electron モジュールの参照を適切に処理してくれる。しかし私は Renderer 側で Node/Electron モジュールを使用しない主義であり、Main プロセスとの通信に必須の ipcRenderer に限定している。

Node/Electron の機能が必要ならば Main プロセスにリクエストすればよい。処理結果も Main プロセスから Renderer プロセス側へ返せる。よって Renderer の targetweb とした。

{
  target: MAIN ? 'electron-main' : 'web`
}

Renderer の require について。ipcRenderer を使用するために require が必要となる。しかしこれは直に使用せず

const ipc = window.require('electron').ipcRenderer
ipc.send('message', 'Hello world!!')

というように window に属するものとして明示的に呼び出す。こうすると Web フロントエンドとしての require or import と Node/Electron 由来の参照を明確に区別できる。そのため targetweb でも問題ない。これは Borwserify 時代からの対応だが、設計面でも Bundler 処理的にも好ましいと考えており、実際に webpack でもそのまま維持できた。

minify と環境変数 production

使用してる npm の項でも触れたが、今回のビルド設定では minify に babel-minify-webpack-plugin を使用している。これは babel-minify の wrapper である。これはかつて babili と呼ばれており、本ブログでも

で紹介したことがある。Babel ファミリーのツールだけあって ES.next を考慮した minify を実行してくれる。

Browserify と組み合わせると Babel (babelify) から先に実行されるため、Bundler 部分のコードが minify されない。単独 CLI だと Babel 処理に組み込めない。…といった問題があってイマイチだったのだが webpack だと Bundler 処理の一部として実行されるため、問題がすべて解決されている。

webpack の設定としては

{
  plugins: PROD ? [
    new MinifyPlugin({
      replace: {
        'replacements': [
          {
            'identifierName': 'DEBUG',
            'replacement': {
              'type': 'numericLiteral',
              'value': 0
            }
          }
        ]
      }
    }, {}),
    new WebPack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('production')
    })
  ] : [
    // development
  ]
}

となる。第一引数に babel-minify のパラメーター、第二引数でプラグイン独自のパラメーターを指定する。今回はデバッグ時の if-def 変数 DEBUG を無効化するようにした。この変数については

で言及しているので詳細はそちらを参照のこと。簡単に要約すると

if (DEBUG) {
  console.log('DEBUG MODE!!!')
}

みたいにデバッグ専用の処理を定義しておき、リリース用ビルドで処理をまるごと除去するためのもの。古式ゆかしいコンパイル分岐である。

リリース用ビルドにおける production 環境変数について。

npm によっては process.env.NODE_ENVdevelopmentproduction であることを判定してコンパイル分岐してる。本来ならさきほど紹介した例もこの方法で実装すべきだったが、それは将来の課題ということで。

process.env.NODE_ENV を変更する場合、CLI 実行であれば cross-env か webpack 処理で代入することになるのだが、標準プラグイン DefinePlugin で対応するほうがよいだろう。

DefinePlugin にしておけば他に環境変数を設定したいときも、ここへ局所化できる。webpack 公式でも Production の Specify the Environment でこの方法を紹介している。

これらの設定で minify すると適切に圧縮され、mangle による副産物としての難読化もおこなわれていることが確認できるはず。

original-fs

音楽プレーヤーのサンプルで Electron の original-fs を参照していたのだが、webpack の targetelectron-main にしていてもこれがエラーになる。

ERROR in ./src/js/main/model/MusicMetadataReader.js
Module not found: Error: Can't resolve 'original-fs' in '.../examples-electron/audio-player/src/js/main/model'

Electron は 2 種類の fs を提供しており original-fsasar をサポートしない Node 本来のもの。その処理を実装していた当時は Electron 版 fs だとなにか問題が起きると勘違いして明示的に original-fs を使用したほうがよいと考えていた。

しかし asar に関する処理をしないのであれば fs でもよいため、そのように修正した。

original-fs が本当に必要となったときは困りそうだから、後で webpack の issues を探してこの問題が報告されていなければレポートを挙げるかもしれない。

まとめ

webpack を使用しはじめたのは最近であること、Electron アプリで Bundler 処理する場合は Node と Web フロントエンドの両方を考慮しなければならないことから、かなり不安があった。

しかし Browserify 関連のツール チェインで要件を洗い出せていたのは大きかった。この種のツールに戸惑うのは要件が定まっていないからである。先に目的を明確にしていれば利用や代替はさほど難しくない。必要最小でよいのだし。今回もそんな感じでわりとスムーズに移行できた。

あと webpack の Environment Variables 機能のおかげで想像よりもずっとシンプルな設定にできて嬉しい。webpack を npm-scripts から実行する派ならファイル分岐を減らすのに効果抜群なのでかなりオススメ。

そういえば Electron サンプルとして examples-electron を公開してるけど、ずっと 3 プロジェクトなのもさみしいので簡易ファイラーを追加したいと考えてる。前から自分用に画像ビューアーがほしいと思ってて、その習作としてもよいのでは?というのがその理由。

ただ examples-electron は他にも

  • Flux を material-flux から reduxalmin に移行
    • 機能的に不満はないが Browserify から webpack へ移行したように一般的で勢いのあるものにしたい
    • redux は既に一般教養となってるから好みとは別に触れておくべきだろう
    • redux を class base にしたような almin も興味ある、なにしろ material-flux と同じ作者だ
  • AltCSS を Stylus 以外へ移行
    • AltCSS 系では Stylus が最も気に入っているけど開発側の熱は失われかけているため移行を検討したい
    • 2017/12 時点だと postcss が有力候補だろうか?
    • ただし Stylus 単体で実現していた機能を postcss のプラグインをかき集めて実現するのは面倒そうだ
    • 大きな問題が起きるまでは Stylus でよい、という考えもある
  • Test Runner を ava へ移行
    • 現在は mocha だが、これと組み合わせてる power-assert を ava が内部利用してるので出力機能としては同等
    • ava の機能としては並列実行に惹かれる
    • mocha も mocha.parallel があるけど ava だと単体で多機能なのでツール チェインを削減できる

といった課題がある。足回りを先にしたほうが生産性あがるのでサンプル追加より環境面を優先するかもしれない。その場合 redux/alumin 移行が作業と学習量として重く時間かかりそう。

npm icon-gen v1.2.0 release

2017年11月21日 0 開発 , ,

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

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

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

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

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

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

PackBits

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

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

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

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

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

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

ICNS 専用 RLE

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

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

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

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

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

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

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

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

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

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

          pos += size
        }

        resolve()
      })
    })
  }
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

iconutil の出力形式

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

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

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

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

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

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

まとめ

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

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

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

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

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

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

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

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

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