Subscribed unsubscribe Subscribe Subscribe

Islands in the byte stream

Technical notes by a software engineer

PlantUML Web APIをSpark Framework + Kotlinなウェブアプリにしてみた

PlantUML を使うとき、 java -jar plantuml.jar だとほんの小さなダイアグラムの生成にも数秒かかってしまいます。ローカルではそれでいいかもしれませんが、ウェブアプリに組み込むのはちょっとイマイチですよね。

PlantUMLはJava製ツールなのでJavaのウェブアプリにしてしまえば高速になるはず、と思ってやってみました。

リポジトリ: https://github.com/gfx/plantuml-service

ウェブアプリといってもパスが /svg/:source しかないような最小限のものです。 :source はPlantUML Text Encodingでエンコードされたソース、または生のPlantUMLソースです。特にキャッシュなどはしませんが、手元のMBAだと小さなダイアグラムの生成で70msほどになったので実用に耐えそうです。

PlantUMLは plantuml-serverというウェブアプリも存在しますが、これはどちらかというとデモなので、純粋にもう少し小さなものが欲しかったのでした。

ウェブアプリケーションフレームワークとしては、SinatraライクなSparkを使ってみましたが、この程度の小さなものだとJettyを直接使っても大差ないかもしれません。Kotlinで書いたのは完全に趣味です。またビルドツールとしてはmavenではなくgradleを使っています。

これはデモも兼ねてHerokuにデプロイしています。Gradleで依存性の解決を行っているので特にKotlin由来の問題はありません。いつものHerokuアプリのように以下のファイルを用意するだけですね。

  • app.json
  • Procfile
  • system.properties (Java8を明示するため)

あとは Deploying Gradle Apps on Heroku | Heroku Dev Centerにしたがって、 build.gradlestage というタスクを用意して、そこでビルドを行うようにします。

以上。まだこれ自体はプロダクションに導入しているわけではありませんが、当初の目的であった高速化は達成できました。PlantUMLをJVMで動くウェブアプリにするというアイデアは効果がありそうです。

TypeScript 2.0+NodeJSツールを開発するときのプロジェクト構成

npm dependenciesを更新してGitHub Compare Viewのリンク付きでPRするツールを定期実行する - Islands in the byte stream

このci-npm-updateはTypeScript 2.0 (beta) で書いたので、TypeScript+NodeJSツールを開発するときのプロジェクト構成の一例としてざっと解説しておきます。

最近はRailsなどのウェブアプリのJSもnpmで管理するようになったため、そういう条件でNodeJSツールを開発することも増えてくることでしょう。

Table of Contents

エディタ

Visual Studio CodeはさすがにTypeScriptサポートはしっかりしていて、次のようにTypeScriptの型定義にしたがって補完をだせます。補完の際に引っかかりを感じることもないですし、構文エラーの指摘なども迅速です。

f:id:gfx:20160731215342p:plain

ただし今回はTypeScript 2.0 betaを使いたいので、 npm install --dev typescript@beta で入れたtypescript language service*1を使うような設定が必要です。

今回は .vscode/settings.json に、以下のようなJSONを設定しました。

{
    "typescript.tsdk": "./node_modules/typescript/lib/"
}

vscodeで本格的にプログラミングするのが初めてなら、ざっとドキュメントも目を通しておくといいでしょう。

とりあえずvscodeで最初におぼえるべきショートカット (for Mac) は command + shift + p (コマンドパレット)です。これはIntelliJ IDEAでいうところの command + shfit + a で、エディタコマンドを検索して実行するためのものです。最初はショートカットを無理におぼえるのではなく、コマンドパレット経由で呼び出すのが楽だと思います。

tsconfig.json

tsconfig.jsonはTypeScript compiler (tsc) に与える設定です。これを適切に設定しておくと、引数なしでtsc(1)を起動してもよしなにビルドしてくれます。

リファレンス:

tsconfig.json · TypeScript

ci-npm-updateでの設定:

ci-npm-update/tsconfig.json at master · gfx/ci-npm-update · GitHub

またtsconfigを作るときにプロジェクトの構成と配布形式を意識することになります。

つまり、今回はnpm distとして配布するので、TypeScriptソースファイルは配布物に含めず、ビルドしたJSと型定義ファイル *.d.ts をパッケージに含めます*2。また開発中は度々ビルドしてJSと型定義ファイルを生成するので、その成果物をどこに置くかを決めます。

成果物はデフォルトだとTSファイルと同じディレクトリにおかれます。このへんは好みだと思うのですが、私はこの挙動を変えたかったので、以下のような構成にしました。

  • src/ - TSファイル。npm distには含めない
  • lib/ - JSファイルと型定義ファイル。npm distに含める

そしてこれにしたがって outDir を設定します。

TSLint

コーディングスタイルを強制するツールです。

Visual Studio Codeのエクステンションを入れると編集中に報告してくれるのではかどります。

TSLint | Visual Studio Marketplace

なおtslintは "tslint:recommended" というプリセットがあるのですが、Visual Studio Codeのデフォルトのフォーマットルールだと警告をだす設定があり、一部変えています。なるべく奨励と銘打たれたスタイルで行きたいのですが、難しいものですね。

ci-npm-update/tslint.json at master · gfx/ci-npm-update · GitHub

Task Runner

今回はシンプルにしたかったし、TSファイルをビルドするだけなのでgulpやgruntは使ってません。そのかわり、npm scriptsを設定してコマンド一つでビルドできるようにしています。

ci-npm-update/package.json at master · gfx/ci-npm-update · GitHub

ビルドの際にtslintも通すようにしているので、たとえばmissing semicolonのよようなちょっとしたtslint違反でもビルドが通らないのですが、このへんは慣れれば減ってくるはずです。

Visual Studio Code Tasks

npm scriptsをvscodeから起動するために .vscode/tasks.json を設定しています。

リファレンス:

Tasks in visual Studio Code

プロジェクトの設定:

ci-npm-update/tasks.json at master · gfx/ci-npm-update · GitHub

(このJSONにはコメントを書けるようだ…)

開発中よく使うのは command+shift+b のビルドコマンドでした。

shrinkwrap

このプロジェクトでは npm-shrinkwrap.json をコミットしていますが、プロジェクトのドッグフーディングのために入れているだけです。。普通のCLIツールやライブラリではこれは不要なので真似しないでください。

所感

TypeScript 2.0とその開発環境は非常に良く出来ていると感じます。もはやES5とは別物といっていいくらい快適ですし、ES2015とくらべても生産性(かけた時間に対する品質のよさ)が高いと思います。

ウェブアプリでもどんどん使っていきたい。

See Also

*1:型定義に基づいた補完などを行うサービスで、TypeScript配布キットに付属しています。

*2:TSファイルも配布物に含めたいところですが、vvakame氏いわく、それをするとtscコンパイラがTSファイルと型定義ファイル両方みにいってしまうという問題があるとか。

npm dependenciesを更新してGitHub Compare Viewのリンク付きでPRするツールを定期実行する

タイトルに要素を詰め込みすぎましたが、要は CircleCIを使ってbundle updateを定期実行する - Qiitanpm update 版です。web appのJavaScriptライブラリ管理にnpmを使うとき、依存関係のアップデートを継続的に行うためのツールです。

https://github.com/gfx/ci-npm-update

これはいまのところGitHub専用です*1。CIサービスはドッグフーディングも兼ねてCircle CI用の起動スクリプトを同梱してますが、実際には ci-npm-update を定期実行するだけなので簡単に代替可能です。

これはCircle CI + Heroku Schedulerで動かしていて、以下のようなフローです。Circle CIはJenkins含め他のCIでも動かせますし、Heroku Schedulerはcronなどでも代替できます。

  1. Heroku Schedulerはci-npm-updateを実行するためのパラメータを渡してCircle CIのビルドを起動する
  2. ビルドのdeploymentフェーズで ci-npm-update --execute を実行する
  3. ci-npm-updateは npm-shrinkwrap.json を削除したあと npm install をしなおす((npm update --depth 9999 のほうが効率がいいはずなのですが、こちらはある程度の規模のプロジェクトだとOOMで落ちてしまうので npm install しなおすことにしました。))
  4. ci-npm-updateはさらに npm shrinkwrap で依存関係のバージョンを固定し、変更をcommitし、pushし、githubのpull-requestをつくる
  5. このときpull-requestはアップデートの前後のnpm-shrinkwrap.jsonをもとにgithub compare viewへのリンクをつくり、PRの本文に入れる

送られるpull-requestはこんな感じです。

https://github.com/gfx/ci-npm-update/pull/8

f:id:gfx:20160728000115p:plain


さて、それではこれを定期的に実行する必要がありますが、これは元エントリのとおりCircle CIのparameterized buildをHeroku Schedulerで起動します。

そのアプリのテンプレートも同リポジトリに入っているので、簡単に試したければ "Deploy to Heroku" ボタンで適当にHeroku appをつくり、Heroku Schedulerで定期実行します。たとえばci-npm-update自身の設定はこんな感じ。

f:id:gfx:20160728000608p:plain

あとは各種設定が必要ですね。

設定

Heroku

"Deploy to Heroku" ボタンでアプリを生成するときにCIRCLECI_TOKEN などの設定が必要なので、それらを指定してください。

TRIGGER_NAME は circle.yml でci-npm-updateを起動する条件分岐に使います。必要なければデフォルトでかまいません。

circle.yml

Heroku appの TRIGGER_NAME が渡ってきたらci-npm-updateを起動する、という設定を追加します。

ci-npm-update自身の設定はこんな感じです。

https://github.com/gfx/ci-npm-update/blob/master/circle.yml

抜粋:

deployment:
  update-dependencies:
    branch: master
    commands:
      - >
        if [ -n "${NPM_UPDATE}" ] ; then
          bin/ci-npm-update --execute
        fi

Circle CI

Circle Ciからは、(1) github repoにgit pushする権限 (2) github repoにpull-requestを送る権限 がそれぞれ必要です。

まずpushする権限をつくるために、repoに対してread/writeできるuser keyを作ります。

Circle CIの Project Settings -> Checkout SSH key -> Add user key

f:id:gfx:20160728002103p:plain

次に、GitHubのaccess tokenを生成してCircle CIに設定します。

GitHub Settings -> Personal access tokens で "repo" scopeをもつaccess tokenを生成し、 Circle CI projectの環境変数で GITHUB_ACCESS_TOKEN という名前で設定してください。

また、Circle CIでgit commitをするために git config user.namegit config user.email が必要なので、それぞれ環境変数GIT_USER_NAMEGIT_USER_EMAILを通じて設定します。

設定が済むとこんな感じです。

f:id:gfx:20160728091520p:plain

検証

もろもろうまくいっていれば、以下のコマンドでHeroku Schedulerを待たずにci-npm-updateを起動でき、pull-requstが送られるはずです。

heroku run -a $app './build-circleci'

成功すると、create pull-request apiの結果であるJSONが表示されます。

また、ローカルで実行する場合は、無引数で実行するとdry runモードで実行しPRの内容を確認できます。ただし、dry-runモードでも node_modules/ の内容はアップデートされるのでご注意ください。

まだ作ったばかりで導入実績も少ないですが、よければ使ってみてフィードバックをいただけると嬉しいです。

*1:github enterpriseでも動くはずですが試してません。他のgit hosting serviceも要望があれば対応するかも。

ExcelからtextareaにコピペするとHTMLのtableに変換するスニペット

ユースケース

  • Excelから社内ブログやWikiに表をコピペしたい

実装

とりあえず paste イベントをうけて処理するのでそのようにする。今回はCodeMirrorで制御されているtextareaなので、CodeMirrorを使ってないない場合は適宜読み替えてください。

ClipboardData

pasteで発行されるClipBoardEventに clipboardData: DataTransfer プロパティがあります。

spec: https://www.w3.org/TR/clipboard-apis/

// Web IDL
dictionary ClipboardEventInit : EventInit {
  DataTransfer? clipboardData = null;
};

こいつが items: DataTransferItemList をもっています。

// Web IDL
interface DataTransfer {
  attribute DOMString dropEffect;
  attribute DOMString effectAllowed;

  [SameObject] readonly attribute DataTransferItemList items;

  void setDragImage(Element image, long x, long y);

  /* old interface */
  [SameObject] readonly attribute DOMString[] types;
  DOMString getData(DOMString format);
  void setData(DOMString format, DOMString data);
  void clearData(optional DOMString format);
  [SameObject] readonly attribute FileList files;
}

なぜクリップボードの中身がリスト状の items になっているかというと、コピーしたものの表現が単一ではないからですね。

たとえば、ブラウザからのコピペはスタイルが適用されているので、 text/plain なプレインテキストと text/html なHTMLテキスト両方を含みます。画像の場合、参照URLがtext/plainで、画像のバイナリデータがimage/*で来ます。ただし、詳細はブラウザごとの違いも多く、たとえばLibreOfficeのスプレッドシートからのペーストは、Chromeだと画像化したtext/pngなitemが含まれますが、IEやFirefoxではテキストデータのみ存在します。

// Web IDL
interface DataTransferItemList {
  readonly attribute unsigned long length;
  getter DataTransferItem (unsigned long index);
}

さて、このitemsの要素は DataTransferItemで、クリップボードの中身の実体です。kindは文字列かファイルかという情報で、typeがMIME typeです。ただし、Firefoxなどitemsの存在しないブラウザもあります。まだ仕様が標準化されていないからでしょう。

// Web IDL
interface DataTransferItem {
  readonly attribute DOMString kind; // "string" or "file"
  readonly attribute DOMString type; // "text/plain", "image/png", ...
  void getAsString(FunctionStringCallback? _callback);
  File? getAsFile();
};

callback FunctionStringCallback = void (DOMString data);

ここで、ExcelのシートからChromeにペーストすると、 items の中身は text/plain, text/html, image/png が入っており、最後の画像データは該当部分のスクショになっています。Excelの要素をGitHub issuesのtextareaにExcelからコピペすると、画像が貼り付けられるのはそのためです。

画像のコピーの場合、itemstext/plain, image/png なので、text/htmlの有無が画像コピーとの違いとなります。

なので、「画像のコピーは画像としてアップロードして貼り付けて、Excelからのコピペは<table>...</table>に変換して貼り付ける」とするならば、以下のロジックでよさそうです。

  • text/plaintext/html のデータが存在して、 text/html のなかに table 要素が含まれるとき、その table 要素をペーストする

以上を加味すると、ペーストイベントをハンドルするイベントリスナは以下のようになるでしょう。

// JavaScript
var editor = CodeMirror.fromTextArea(...);

editor.on('paste', function(_, e) {
  // see https://html.spec.whatwg.org/multipage/interaction.html
  const clipboardData = e.clipboardData;

  const plainTextItem = clipboardData.getData('text/plain');
  const rtfItem = clipboardData.getData('text/rtf');
  const htmlItem = clipboardData.getData('text/html');
  const imageItem = (function (items) {
    if (!items) {
      return null;
    }

    for (let i = 0; i < items.length; i++) {
      if (items[i].type.startsWith('image/')) {
        return items[i];
      }
    }
    return null;
  })(clipboardData.items);

  console.log(clipboardData.types);
  console.log([plainTextItem, rtfItem, htmlItem, imageItem]);
  if (htmlItem && plainTextItem) {

    // rich contents, such as Excel and Power Point
    const html = new DOMParser().parseFromString(htmlItem, 'text/html');
    const tables = html.getElementsByTagName('table');

    if (tables.length) { // includes <table> tags
      const content = html.getElementsByTagName('body')[0].innerHTML;
      editor.replaceSelection(content);
      e.preventDefault();
      e.stopPropagation();
    } else {
      // TODO: ask users what to paste (HTML or plain text)
      // fallback to default
    }

  } else if (imageItem) {
    const blob = imageItem.getAsFile();
    const matched = blob.type.match(/^image\/(\w+)$/);
    if (matched) {
      const ext = matched[1]; // e.g. "png", "jpeg", or "gif"
      const filename = 'clipboard.' + ext;
      const formData = new FormData();
      formData.append('file', blob, filename);
      upload(formData);

      e.preventDefault();
      e.stopPropagation();
    }
  }
});

というわけで実装してみたものの、だいぶ複雑だしブラウザによる挙動の違いもあるのでちょっと使いづらい部分があります。

TSVやCSVをコードスニペットとして貼り付けるとtableとして表示する、というほうがいいかもしれないですね。

公開用DockerイメージにAndroid SDKを含めるのはライセンス違反という話

Android Software Development Kit License Agreementにこういう項目があります。

3.4 You may not use the SDK for any purpose not expressly permitted by the License Agreement. Except to the extent required by applicable third party licenses, you may not: (a) copy (except for backup purposes), modify, adapt, redistribute, decompile, reverse engineer, disassemble, or create derivative works of the SDK or any part of the SDK; or (b) load any part of the SDK onto a mobile handset or any other hardware device except a personal computer, combine any part of the SDK with other software, or distribute any software or device incorporating a part of the SDK.

まあ要は、変更したり再配布したりリバースエンジニアリングしたりするなってことですね。そして、Docker imageにAndroid SDKを含めて再配布するのはまさにライセンス違反というわけです。

DockerでAndroidアプリをビルドするのがそれなりに広まってきた様子ですが、Android SDKの含まれたイメージをDocker Hubで公開してはいけないということになります。

Android SDKライセンスが現状に追いついていないともいえるかもしれませんが、ライセンスはライセンスです。AndroidアプリをDockerでビルドしている各位はご注意ください。

私もうっかりDocker HubでSDK入のイメージを公開しているのですが、近日中に削除する予定です*1。これらに依存している場合はご注意ください。

削除予定のイメージ

*1:Dockerfileを公開することは問題ないので残します。

Makefileを自己文書化するハックを注入するスクリプト: inject-make-help

Re: Makefileを自己文書化する make2help | おそらくはそれさえも平凡な日々

ぼくもmakeはわりと使う方で、AndroidプロジェクトだろうとiOSプロジェクトだろうとよく使うコマンドセットをMakefileとして追加するのが大好きなんですが、self-documented Makefileは存在はしっていたもののセットアップが面倒でやっていませんでした。

じゃあ make2help はどうかというと、ただひとつのコマンドをインストールすると常self-documented Makefileを使えるという点では優れたアイデアなんですが、じゃあ今度はプロジェクトメンバーに make2help をインストールするように指示しないといけなくなってしまうのがイマイチだなあと。makeのメリットの一つは、敢えてインストールしなくてもだいたい入っているという点だと思うので。

というわけで、self-documented Makefile hackをMakefileに注入するスクリプトを書きました。まあホントはこのhackもgrep(1) とか awk(1) ではなく perl(1) で書き直したいところですが、とりあえずオリジナルのものを注入するだけです。すでに注入済みなら何もしません。

これなら、誰か一人が inject-make-help を実行したあとの Makefile をコミットしておけば誰でも make help が使えます。どうでしょう。

#!/usr/bin/env perl
# inject-make-help(1): To inject help hacks to the Makefile
use 5.10.0;
use strict;
use warnings;
use autodie;

# FIXME: use perl(1) instead of grep(1), sort(1), and awk(1)
my $help_task = q{
help:
    @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
} =~ s/^[ ]{4}/\t/xmsgr;

my $makefile = shift(@ARGV) // 'Makefile';

my $content = do {
    local $/;
    open my $fh, '<', $makefile;
    <$fh>;
};

if ($content !~ /\Q$help_task/xms) {
    $content .= $help_task;
    
    my($phony_tasks) = ($content =~ /^\.PHONY:([^\n]+)/xms);
    if (not defined $phony_tasks) {
        $content .= q{
.PHONY: help
} =~ s/^[ ]{4}/\t/xmsgr;
    } elsif ($phony_tasks !~ /\bhelp\b/xms) {
        $content =~ s/^(\.PHONY:[^\n]+)/$1 help/xms;
    }

    say "Inject help triks to $makefile";
    open my $fh, '>', $makefile;
    print $fh $content;
    close $fh;
}

ターゲットの Makefile はこんな感じ:

.DEFAULT_GOAL := help

foo: ## The foo task
  echo foo

bar: ## The bar task
  echo bar

これに対して inject-make-help をするとこんな感じになります:

.DEFAULT_GOAL := help

foo: ## The foo task
  echo foo

bar: ## The bar task
  echo bar

help:
   @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

.PHONY: help

.PHONY は既存のものがあればそれに対して help を追加します。

git branch の結果を時間順にソート: git branch --sort=-authordate

id:kazuho さんと「gitのbranchを消すべきか否か」という話をしていて、ぼくの「ローカルにせよリモートにせよbranchが増えすぎると目的のブランチを見つけられない」という意見に対して次のエントリを教えてもらったのでした。

git branch の結果を時間順にソート - kazuhoのメモ置き場

一理あるかもしれないと思ってこれをgitに組み込むためにgitのソースコードを眺めていたら、実はもうできるということを知りました。それがこれ:

# 新しいのが下
git branch --sort=authordate

# 新しいのが上
git branch --sort=-authordate

このソートに使えるフィールドは、 git branch --help を引くと "The keys supported are the same as those in git for-each-ref." といわれるので、 git for-each-ref --help を引くとそこにいろいろ書いてあります。

なおソースだとこのあたりで定義されているようです。

git/ref-filter.c at v2.8.4 · git/git · GitHub