Redmine テーマ minimalflat2 v1.3.0 リリース

2017年7月16日 0 開発 , ,

Redmine 3.4 がリリースされたので minimalflat2 も対応した。

以下、開発メモ。

Stylus 定義を標準 application.css にあわせる

minimalflat2 の CSS は Stylus で記述して application.cssresponsive.css へコンパイルしている。Stylus の代表的な機能には透過的な変数参照、Mix-In、クラスのネストがあってこれまで便利に利用してきたのだけど、本バージョンからネストは控え目にした。

ネストによってクラス定義の冗長さは軽減される。例えば

#top-menu {background: #3E5B76; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
#top-menu ul {margin: 0;  padding: 0;}
#top-menu li {
  float:left;
  list-style-type:none;
  margin: 0px 0px 0px 0px;
  padding: 0px 0px 0px 0px;
  white-space:nowrap;
}

のように同一 id やクラスを親としているものは

#top-menu {
  background: #3E5B76; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;

  ul {
    margin: 0;  padding: 0;
  }

  li {
    float:left;
    list-style-type:none;
    margin: 0px 0px 0px 0px;
    padding: 0px 0px 0px 0px;
    white-space:nowrap;
  }
}

のようにネスト可能。ただしこれを徹底すると標準 application.css と定義位置が乖離してゆき、差分比較して追従するのが難しくなる。また、ときには位置の依存関係が崩れることで意図せぬ問題を引き起こす。

以上の理由からアイコン フォント用の疑似要素などを除き、標準 application.css の定義順へならうことにした。Redmine のバージョン更新があれば、最後に対応したものと最新版の application.css を差分比較して部分対応すればよい。従来もそうしていたのだが、今回の変更によりこれが更に容易となった。

Redmine v3.4 の Vagrant Box

テーマ開発における Redmine 上の動作確認は onozaty さん が提供している Vagrant Box を利用している。今回も Redmine v3.4 版が公開されたので、それを Vagrantfile へ指定するようにした。

Twitter 上で Vagrant Box リリースされないのかな?とつぶやいていたら迅速に対応していただけた。非常にありがたい。

Redmine v3.4.0 から間隔が空いたのは、これのすぐ後に致命的なバグを修正した v3.4.1 がリリースされたので、これを待っていたのかもしれない。

Redmine v3.4 プロジェクト一覧の謎

minimalflat2 では theme.js によりプロジェクト一覧をツリー上に開閉する機能がある。しかし Redmine v3.4 へ更新したら、これがうまく動作しない。そもそもプロジェクト名と説明文が横並びになったりする。

もしかして HTML の DOM 構造が大幅に変更された?標準テーマではどうだろう?と試したら、標準のほうでもそうなる。これは application.css にある

#projects-index {
  column-count: auto;
  column-width: 400px;
  -webkit-column-count: auto;
  -webkit-column-width: 400px;
  -webkit-column-gap : 0.5rem;
  -moz-column-count: auto;
  -moz-column-width: 400px;
  -moz-column-gap : 0.5rem;
}

という定義が原因だった。

複数カラムでグリッド状に表示するための定義らしいけど、responsive.css のほうは従来どおり縦一列である。表示幅が広ければグリッドで、ということなのだろう。しかしプロジェクト名に対して説明文が回り込んでしまうなど、好ましくない表示のされかたをする。また minimalflat2 としてはツリー表示によりプロジェクト一覧を整理する方針なのでグリッドにしなくても冗長さはおさえられる。

以上の理由から、この新しい定義は無効化することにした。プロジェクト一覧は表示幅にかかわらず、従来どおり常に縦一列になる。

モバイル用メニューのアイコン フォント対応強化

Redmine v3.4 ではアイコン画像を表示する DOM 要素に icon- を接頭辞とするクラスが統一的に指定されるようになった。指定されるだけで CSS に画像指定のないものも多数あるのだが、minimalflat2 としてはなるべくこれらにアイコン フォントを割り当てるようにした。

もっとも目立つのはモバイル用メニューのアイコンだろう。サイド メニューから移動されてきた項目以外はのきなみ icon- 接頭辞をもつため、かなり華やかになった。

モバイル用メニュー

簡易テスト用 HTML 更新

Vagrant なのか Redmine の設計なのか分からないが、VM のテーマ ディレクトリーと同期している場所で CSS が更新されても Redmine に反映されない。Web ブラウザーのリロード、スーパー リロードをしてもダメで、しかたなく vagrant reload している。

しかしこれは非常に時間がかかる。そのため Redmine の代表的な画面を静的 HTML として保存し、そこにコンパイルされた application.css などを読ませるようにして簡易テストできるようにしている。

今回も Redmine v3.4 用に HTML を保存し直して更新した。また従来のリポジトリー構成では

src/
├── debug_images/
├── favicon/
├── fonts/
├── images/
├── javascripts/
├── stylesheets/
├── stylus/
└── *.html

のように src/ 直下に全ファイルが並んでいて stylus のように開発で頻繁に書き換えるものと、静的でほとんど更新のないものが区別しにくかった。そこで

src/
├── assets/
│   ├── debug_images/
│   ├── favicon/
│   ├── fonts/
│   ├── images/
│   ├── stylesheets/
│   └── *.html
└── stylus/

のように静的リソースは assets/ へ置くようにした。最近の Web フロントエンドや Electron アプリ開発でもこのようにしている。Stylus がコンパイルした CSS と Source Maps は assets/stylesheets/ へ出力される。

assets/ がテーマとして動作するための構成をもったディレクトリーとなる。リリース用イメージ生成も、ここにあるものから必要なファイルをコピーすればよい。動的なファイルは Stylus のコンパイル結果ぐらいなので、cpx 用のフィルターも書きやすくなった。

gulp で uglify-es を利用する

2017年6月14日 0 開発 , ,

タスクランナーは npm-scripts 派なのだけど、akabekobeko/examples-web-app に公開している front-end-starter には gulp 版も用意してある。かつて gulp を利用していたこと、世間では現在もそれなりに Web フロントエンド開発で gulp が採用されていることから動向チェック用にメンテナンスしている。

front-end-starter は Electron も含む Web フロントエンド開発環境の変更を気が向いたときに反映しているのだが、そういえば uglify-es を gulp で利用したくなったらどうするのだろう?というのが気になったので試してみた。

gulp-uglify

gulp で uglify-js を利用する場合、gulp wrapper となる gulp-uglify を採用するのが一般的だろう。では uglify-es はどうなのか。uglify-js 本家が uglify-es を独立したパッケージとしているように gulp-uglify-es があるとか?と予想したが gulp-uglify として js/es を切り替えられるようになっていた。

README の Using a Different UglifyJS から引用する。

By default, gulp-uglify uses the version of UglifyJS installed as a dependency. It’s possible to configure the use of a different version using the “composer” entry point.

標準では dependency としてインストールされた uglify-js を使用するが、composer を利用することで js/es を分岐できるとのこと。以下は README に併記されているサンプル コード。

var uglifyjs = require('uglify-js'); // can be a git checkout
                                     // or another module (such as `uglify-es` for ES6 support)
var composer = require('gulp-uglify/composer');
var pump = require('pump');

var minify = composer(uglifyjs, console);

gulp.task('compress', function (cb) {
  // the same options as described above
  var options = {};

  pump([
      gulp.src('lib/*.js'),
      minify(options),
      gulp.dest('dist')
    ],
    cb
  );
});

gulp ストリーム (pipe) をそのまま使用せず pump で wrap している点を除けば、割りと簡単に切り替えられるようだ。

gulp タスク修正

gulp-uglify としては pump を推奨しているようだが、以下のように gulp-watchify と組み合わせていると書き換えが多くなるので、

// ...import や gulp-load-plugins の設定など

gulp.task('js', $.watchify((watchify) => {
  const time = process.hrtime()

  return gulp.src([ config.dir.js + 'App.js' ])
    .pipe($.plumber())
    .pipe(watchify({
      watch: config.isWatchify,
      basedir: './',
      debug: true,
      transform: [ 'babelify' ]
    }))
    .pipe(VinylBuffer())
    .pipe($.if(!(config.isRelease), $.sourcemaps.init({ loadMaps: true })))
    .pipe($.if(config.isRelease, $.uglify()))
    .pipe($.rename('bundle.js'))
    .pipe($.if(!(config.isRelease), $.sourcemaps.write('./')))
    .pipe($.if(config.isRelease, gulp.dest(config.dir.dist), gulp.dest(config.dir.assets)))
    .on('end', () => {
      const taskTime = PrettyHRTime(process.hrtime(time))
      $.util.log('Bundled', $.util.colors.green('bundle.js'), 'in', $.util.colors.magenta(taskTime))
    })
}))

gulp ストリームのまま移行する。

// ...import や gulp-load-plugins の設定など

// 追加
import UglifyES from 'uglify-es'
import UglifyComposer from 'gulp-uglify/composer'

gulp.task('js', $.watchify((watchify) => {
  const time = process.hrtime()

  // uglify-es で minify するための関数設定
  const minify = UglifyComposer(UglifyES, console)

  return gulp.src([ config.dir.js + 'App.js' ])
    .pipe($.plumber())
    .pipe(watchify({
      watch: config.isWatchify,
      basedir: './',
      debug: true,
      transform: [ 'babelify' ]
    }))
    .pipe(VinylBuffer())
    .pipe($.if(!(config.isRelease), $.sourcemaps.init({ loadMaps: true })))
    // uglify-es で minify
    .pipe($.if(config.isRelease, minify({})))
    .pipe($.rename('bundle.js'))
    .pipe($.if(!(config.isRelease), $.sourcemaps.write('./')))
    .pipe($.if(config.isRelease, gulp.dest(config.dir.dist), gulp.dest(config.dir.assets)))
    .on('end', () => {
      const taskTime = PrettyHRTime(process.hrtime(time))
      $.util.log('Bundled', $.util.colors.green('bundle.js'), 'in', $.util.colors.magenta(taskTime))
    })
}))

このサンプルには掲載していないが、別のリリース用イメージ生成タスクで config.isReleasetrue にしてから js タスクを実行することで、uglify-es による minify 処理が走ることを確認できた。

まとめ

Electron や Chrome (Chromium) のように先進的な環境を決め打ちで開発したり、babel-preset-env で環境を抽象化しつつ変換を最小におさえると minify 対象となるコードに ES.next 部分があらわれる。その際は minify ツール側も ES.next 対応が求められるため uglify-esbabili を利用することになるだろう。gulp に関しては本記事のように gulp-uglify の機能として uglify-es を採用するとよい。

ところで gulp-uglify の設計は gulp wrapper における問題へのよき回答にもなっており、感心した。

gulp を想定していない npm を gulp ストリームにのせる場合、vinyl-buffer などで頑張るか、そのような処理で wrap した gulp プラグインとする必要がある。前者はエンド ユーザー負担が大きすぎるので後者を選ぶことが多いのだけど、こちらだと gulp プラグインの dependency で wrap 対象 npm の更新へ追従しなければならない。

gulp-uglify/composer 方式なら自身の dependency が古びても、エンド ユーザー側から任意バージョンの npm を指定できる。つまり gulp 化の恩恵を享受しつつ、最新 npm に置き換えられるのだ。対象 npm のインターフェースが維持されていることは前提となるが、うまい方法である。自身の dependency + 同一インターフェース npm をサポートできる。

私がタスクランナーを gulp から npm-scripts へ移行した理由のひとつは gulp プラグインの陳腐化なのだけど、この設計が普及すれば、この問題に関しては概ね解決しそうである。gulp プラグインのガイドラインに含めてもよいのでは、とも思う。

この記事を書いた後に気付いたのだが babili の gulp 版として gulp-babili が提供されている。こちらは Babel/babili 本家なので、Babel ファミリーを好むなら uglify-es の代りに採用してもよいだろう。

Electron を試す 9 – Babel 変換を最小におさえつつ minify

2017年6月5日 0 開発 , ,

小ネタ。

Electron が採用している Chromium は ECMAScript 対応がかなり進んでいる。よって Babel を使用しつつも変換を最小におさえたくなる。

この点について以前 babel-preset-env と minify という記事を書いたのだが、uglify-js の ES2015 以降への対応が暫定版なため、よりよい選択肢として babel-preset-babili を試してみた。その記事のコメントで mysticatea さんが提案されているように Browserify へ -g オプションをつければ node_modules 部分も含めて minify 可能だが、それでも Browserify の Bundle 処理は minify されない。

よって uglify-js harmony 版が正式リリースされるのを待っていたところ、uglify-es が提供されたので試してみる。

uglify-es

従来 uglify-js で harmony と呼ばれていた ES2015 以降へ対応する仕向け。README によれば uglify-js@3.x 系に対して API と CLI 互換がある。CLI 名も uglifyjs のまま。よって最新の uglify-js を使用しているなら、特に処理を変えず移行可能。

移行は package.jsondevDependencies で uglify-js を uglify-es へ置き換えればよい。uglify-js@3.x から提供となるので、バージョン系は 3.x 以降となる。安全のため npm un -D uglify-js してから npm i -D uglify-es するのがよいだろう。

余談だが Support UglifyJS 3 と関連 issue にて webpack の uglify-es 対応が検討されている。難航しているようだ。

babel-preset-env

uglify-es により ES2015 以降を解析可能となるため、Babel 変換も最小にする。babel-preset-env へ開発で使用している Electron のバージョンを指定。以下は package.json の例。.babelrc なら "babel" プロパティの値をルートにする。

"babel": {
  "presets": [
    [
      "env",
      {
        "targets": {
          "electron": "1.6"
        }
      }
    ]
  ]
}

この設定で akabekobeko/examples-electron のプロジェクトをビルドして classconstlet といった ES2015 以降の機能はそのままに uglify-es で minify されることを確認できた。

問題点

uglify-es は ES2015 なら対応しているけれど async/await を含むコードはエラーになる。試しに async function – JavaScript | MDN のサンプルを含めてみたところ、

Parse error at 0:2932,6
async function add1(x) {
      ^
ERROR: Unexpected token: keyword (function)
    at JS_Parse_Error.get (eval at <anonymous> (.../examples-electron/audio-player/node_modules/uglify-es/tools/node.js:21:1), <anonymous>:86:23)
    at fatal (.../examples-electron/audio-player/node_modules/uglify-es/bin/uglifyjs:286:52)
    at run (.../examples-electron/audio-player/node_modules/uglify-es/bin/uglifyjs:241:9)
    at Socket.<anonymous> (.../examples-electron/audio-player/node_modules/uglify-es/bin/uglifyjs:179:9)
    at emitNone (events.js:110:20)
    at Socket.emit (events.js:207:7)
    at endReadableNT (_stream_readable.js:1045:12)
    at _combinedTickCallback (internal/process/next_tick.js:102:11)
    at process._tickCallback (internal/process/next_tick.js:161:9)

となった。ちなみに Browserify も以前は es6 async class function fails to parse? という問題があったが修正済み。uglify-es の issue [ES8] async/await not supported also in harmony を読むに、近く修正されそうな気がする。

まとめ

async/await 対応の問題はあるものの、babel-preset-env + uglify-es の組み合わせで Electron 向けの Babel 変換を最小におさえられた。

Chrome Canary 60 はフラグ付きで ES Modules が有効になるそうで、これが Electron に採用されたら Bundle と minify 事情も変わりそう。しかし node_modules も含めたサイズ圧縮の観点から Bundle と minify はしばらく必要な処理なので、今後も動向は継続的にチェックしたい。