Skip to content

Instantly share code, notes, and snippets.

@tsukaby
Forked from seratch/getting_started_ja.md
Last active August 29, 2015 14:16
Show Gist options
  • Save tsukaby/db08d8c4cd6cf3283424 to your computer and use it in GitHub Desktop.
Save tsukaby/db08d8c4cd6cf3283424 to your computer and use it in GitHub Desktop.

NOTICE: This is just a draft of Skinny framework introduction (written in Japanese for now). English version will be published soon.

Skinny Framework とは

Logo

https://github.com/seratch/skinny-framework

Skinny Framework は Scala のフルスタックな Web アプリケーション開発フレームワークです。2014/03 を目処に最初の安定バージョン 1.0.0 をリリースするべく精力的に開発しています。(追記: 2014/03/28 に 1.0.0 がリリースされました)

Skinny Framework の立ち位置として、既に相当に広く認知されたフレームワークとして Play framework が存在しており、また Sinatra 的なものを土台とし glue code で構成されている面では Padrino Framework に似ているともいえます。しかし、Skinny を一言で説明するキャッチコピーとしては「Scala on Rails」を掲げており、主要な API の命名では基本的に Ruby on Rails を踏襲する方針をとっています。

ただし、必ずしも単に Rails のクローンをつくることを目的にはしておらず、メイン開発者である @seratch が理想と考えるフレームワークを追求することを最も重要な判断基準とし、Rails を真似すべきでないと考えた点は若干異なった仕様になっている場合もあります。

モチベーション

Scala の世界では Reactive をキーワードにしたイベント駆動なアーキテクチャがトレンドですが Skinny は Servlet をベースとした技術であり、従来型の Web アプリケーションの開発の生産性、保守性を高めることを目指しています。Scala にはまだそのような用途でのデファクトスタンダードなフレームワークが存在していませんが、潜在的なニーズはとても大きいと考えています。

メイン開発者である @seratch は Scala をこよなく愛しており Scala での開発効率がそれほど高くないという現状に忸怩たる思いがあります。Scala で自分が理想とする Web アプリフレームワークを追求し、それが世界中の開発者に受け入れられることを目標としています。

また Ruby on Rails の場合、Windows 上での開発することは現実的にほぼ不可能ですが Skinny は JVM である利点を活かして Windows のサポートにも力を入れています。Windows ユーザであることが一つの要因となって旧来の Java による開発を続けているような開発者の方々にも生産性の高い Web 開発環境を提供したいと考えています。

Skinny を使った開発の紹介

ここからは Skinny Framework 1.0.6 時点での実装による Web アプリ開発の概要を説明します。

はじめての Skinny アプリケーション

Skinny での Web アプリ開発は、一般的に知られる Web アプリの MVC パターンに素直に従ったものです。Controller、Model、View を定義し、Controller の処理メソッドをルーティング管理にひもづけます。標準として推奨するスタイルはありますが Skinny を導入したとしても特に強制力のある制限は加えられないため Skinny のスタイルの中に素の Scalatra 的な実装を混ぜることも可能です。

skinny-blank-app を入手

早速、始めましょう。まずプロジェクトのひな形は Skinny のウェブサイトからリンクされている skinny-blank-app.zip という zip ファイルをダウンロード、解凍して準備してください。

https://github.com/seratch/skinny-framework/releases

wget https://github.com/seratch/skinny-framework/releases/download/1.0.6/skinny-blank-app.zip
unzip skinny-blank-app.zip
cd skinny-blank-app

解凍したら tree コマンドなどでざっとファイル構成を見てみてください。task と build というディレクトリは後から説明します。それ以外は至って普通の sbt プロジェクトであることがお分かりいただけるかと思います。

早速 ./skinny run でサーバを立ち上げてみてください。

./skinny run

Windows ユーザの方も skinny.bat を使って skinny run で起動できるはずです。普段 Scala の開発をしていないマシンだと色々 jar ファイルをダウンロードする必要があり、少々待たされるかもしれません。初回だけなのでご心配なく。Jetty が起動したのを確認した後に http://localhost:8080/ にアクセスして 200 OK が返ってきたらセットアップは完了です。

デフォルトの run コマンドでは Scalate のテンプレートファイルはファーストアクセス時にメモリ上で変換されます。これにより view 部分の開発をリスタート + Scala のコンパイル待ちなしに行うことが出来ますが、ローカルでアプリの挙動をデモしたい場合などにはマッチしません。そのような場合には ./skinny run -precompile で起動してください。全ての Scalate テンプレートが precompile された状態で起動します(起動前のコンパイル時間は長めになります)。

なお Java Debug Wire Protocol (JDWP) によって IDE のデバッガと連携させたい場合は ./skinny debug または ./skinny d で起動してください。デフォルトでは 5005 ポートを listen します。

コントローラとルーティング

まずトップページがどうやって表示されているかを説明します。

Skinny でのルーティングは Scalatra をそのまま使っていますが、下の例のように #as(Symbol) を呼び出す点が異なります。これについては、後述の beforeAction/afterAction のところで説明しますが、今は Action の定義に Scala のシンボルで名前をつけているということだけ理解しておいてください。

トップページを表示するために RootController というクラスが準備されています。実装は非常にシンプルで、これだけで動作します。

package controller
class RootController extends skinny.SkinnyController {
  def index = render("/root/index")
}

RootController は SkinnyController を継承していますが、この SkinnyController は ServletFilter を実装した trait です。以下の Controllers.root は ServletFilter をルーティング情報付きでインスタンス化したものになります。これを SerlvetContext に mount します。

package controller
object Controllers {
  val root = new RootController with skinny.Routes {
    get("/?")(index).as('index)
  }

  def mount(ctx: ServletContext): Unit = {
    root.mount(ctx)
  }
}

通常の Scalatra アプリだと逆で ServletContext#mount(instance, "/*") のような形になるのですが、これについて Skinny では若干独自で作り込みをしています。その理由は Controller が増えて、かつ他の ServletFilter やライブラリなどと併用した場合にパフォーマンス劣化を招くケースがあることが分かったためで、それを防止する対応を Skinny 側で行っています。Scalatra のスタイルでも動作しますが、基本的には SkinnyController#mount(ServletContext) を使うことをおすすめします。

テンプレートエンジン(Scalate)

次にテンプレートエンジンを見てみましょう。先ほどの RootController 内にあった render の箇所はどのように動作するのでしょうか。

render("/root/index")

という処理があった場合 Skinny Framework は /WEB-INF/views/root/index.html.ssp というファイルが存在し、かつ、それが valid な SSP(Scala Server Pages) のテンプレートファイルであることを期待します。

blank-app では当初は単なる html になっています。

<h3>Hello World!</h3>
<hr/>
<p>Your first Skinny app works!</p>

ベースとなるレイアウトは /WEB-INF/layouts/default.jade にあります。こちらは jade で書かれていますが、拡張子を ssp に変えて中身も ssp に書き換えることも可能です。

試しにここにパラメータをそのまま出すようにしてみましょう。以下のように書き換えます。

<%@val params: skinny.Params %>
<h3>Hello <%= params.name.getOrElse("Anonymous") %>!</h3>

http://localhost:8080/ では「Hello Anonymous!」で http://localhost:8080/?name=Skinny なら「Hello Skinny!」と表示されるはずです。

また、Scalate の弱点として、このような変数が増えてくると先頭で定義するものがどんどん増えてしまう傾向があります(sbt での設定によりある程度省略することもできますが)。そこでただ一つだけ import すれば標準の変数を呼び出せるように skinny.Skinny というクラスが用意されており、デフォルトだと s という名前で渡されるようになっています。

<%@val s: skinny.Skinny %>
<h3>Hello <%= s.params.name.getOrElse("Anonymous") %>!</h3>

こうしておくと Skinny が標準で提供するものはこの s 経由でアクセスすることができます。もしこの s という名前に不都合があれば自分で任意の名前で渡すこともできます。

次に RootController から値を渡す例を見てみましょう。#set は Skinny が提供するリクエストスコープに値を設定するメソッドです。Rails でいえば Controller にインスタンス変数で設定してテンプレートエンジンから参照するのと同様の機能を実現するものです。

def index = {
  set("name" -> params.get("name").getOrElse("Anonymous"))
  render("/root/index")
}

この値は view で以下のように使用します。

<%@val name: String %>
<h3>Hello <%= name %>!</h3>

ここまでの操作で Controller をいじったとき以外は特に長いコンパイル待ちや Jetty のリスタートは発生しなかったと思います。

WEB-INF の下を編集している限りは変更したらブラウザからすぐに確認、という流れで開発することができます。とはいえ、しばらく動かし続けているうちにまれに sbt が OutOfMemoryError になってしまうことがあるので、その場合は Ctrl + C で停止して skinny run で立ち上げ直してください。

また 1.0.59.14 時点で Scalate 以外に FreeMarker と Thymeleaf をサポートしています。Skinny の開発スタイルは View の変更で再コンパイルやリスタートをしないため、これらの Java 用テンプレートエンジンも相性がよいといえます。チームの事情で慣れているテンプレートエンジンを採用してもよいと思います。ただし、これらのテンプレートエンジンでは Scala のメソッドを呼べないケースがあるなど制約もありますので、特に理由がなければまずは Scalate を検討することをおすすめします。

ジェネレータによるコード生成

次に Skinny が提供する generator を使って新しい controller や scaffod を生成してみましょう。とりあえずヘルプページでも作ってみることにしましょう。

./skinny g controller help

[info] Running TaskLancher generate:controller help
"src/main/scala/controller/HelpController.scala" created.
"src/main/scala/controller/Controllers.scala" modified.
"src/main/scala/ScalatraBootstrap.scala" modified.
"src/test/scala/controller/HelpControllerSpec.scala" created.

この状態で http://localhost:8080/help にアクセスするとサンプルページが表示されたと思います。view のファイルは生成されませんでしたが、development モードのファーストアクセスでもしテンプレートファイルが存在しなかったら、デフォルトのひな形が生成されます。このケースでは /WEB-INF/views/help/index.html.ssp が生成されます。

jade を使ってみる

これまで ssp で説明してきましたが ssp は Ruby でいう erb、Java でいう jsp と同じような存在です。もし Haml や Jade になじみのある方であれば scaml や jade を利用してみてもよいかもしれません。ここでは jade を使ってみましょう。

HelpController.scala に以下の一行を加えてください。

override def preferredScalateExtension = "jade"

この状態でブラウザから /help にアクセスすると今度は /WEB-INF/views/help/index.html.jade が生成されます(ここでは自動生成で説明していますが、もちろん手動でファイルを作成しても OK です)。jade を使う場合はもはや index.html.ssp は不要になっているので削除しても構いません。jade に関しては Scalate の scaml のドキュメントと合わせて読むとほとんどのことは網羅されています。

http://scalate.fusesource.org/documentation/scaml-reference.html

http://scalate.fusesource.org/documentation/jade.html

scaffold 生成

Skinny の様々な機能を理解するには scaffold を生成してみるのがよいと思います。

scaffold コマンドは SkinnyResource という Rails の ActiveResource に似た Controller 生成とそれをルーティング情報に追加、Skinny ORM による model クラスと対応する messages.conf の生成、scaffold:{extension} の extension に対応したテンプレートファイルの生成、DB マイグレーションファイルの生成を行ってくれます。

コマンドの指定のどこかにミスがあった場合は Usage が表示されます。入力内容を再確認してください。

./skinny g scaffold:jade members member name:String luckyNumber:Option[Long] birthday:LocalDate alertTime:Option[LocalTime]

  "src/main/scala/controller/ApplicationController.scala" skipped.
  "src/main/scala/controller/MembersController.scala" created.
  "src/main/scala/controller/Controllers.scala" modified.
  "src/test/scala/controller/MembersControllerSpec.scala" created.
  "src/test/scala/integrationtest/MembersController_IntegrationTestSpec.scala" created.
  "src/test/resources/factories.conf" modified.
  "src/main/scala/model/Member.scala" created.
  "src/test/scala/model/MemberSpec.scala" created.
  "src/main/webapp/WEB-INF/views/members/_form.html.jade" created.
  "src/main/webapp/WEB-INF/views/members/new.html.jade" created.
  "src/main/webapp/WEB-INF/views/members/edit.html.jade" created.
  "src/main/webapp/WEB-INF/views/members/index.html.jade" created.
  "src/main/webapp/WEB-INF/views/members/show.html.jade" created.
  "src/main/resources/messages.conf" modified.
  "src/main/resources/db/migration/V20131109015919__Create_members_table.sql" created.

このmembers というテーブルが必要なので生成された migration ファイルを実行します。

./skinny db:migrate

この状態で ./skinny run を起動して http://localhost:8080/members にアクセスすると Bootstrap 3 を使った CRUD 画面が既に使えるようになっているはずです。とても簡単ですね。

SkinnyResource によるコントローラ

それでは生成された MembersController について見てみましょう。生成された override 定義について説明を書き加えてみました。SkinnyResource という Rails でいう ActiveResource に相当する trait をテンプレートメソッドパターンで必要な差分を override する形になっています。

object MembersController extends SkinnyResource {
  // CSRF 対策を有効にします
  protectFromForgery()

  // このリソースに対応する Model object
  //skinny.SkinnyModel を実装していれば Skinny ORM でないものと組み合わせることが可能です
  override def model = Member
  // 複数形
  override def resourcesName = "members"
  // 単数形
  override def resourceName = "member"

  // 新規作成時のバリデーションルール
  override def createForm = validation(createParams,
    paramKey("name") is required & maxLength(512),
    paramKey("lucky_number") is numeric & longValue,
    paramKey("birthday") is required & dateFormat,
    paramKey("alert_time") is timeFormat
  )

  // 新規作成時のパラメータ
  // withDate は birthday{_year/_month/_day} を一つにまとめます
  override def createParams = Params(params)
    .withDate("birthday")
    .withTime("alert_time")

  // Model に渡す永続化すべき属性の一覧を定義します
  override def createFormStrongParameters = Seq(
    "name" -> ParamType.String,
    "lucky_number" -> ParamType.Long,
    "birthday" -> ParamType.LocalDate,
    "alert_time" -> ParamType.LocalTime
  )

  // 更新時のバリデーションルール
  override def updateForm = validation(updateParams,
    paramKey("name") is required & maxLength(512),
    paramKey("lucky_number") is numeric & longValue,
    paramKey("alert_time") is timeFormat
  )

  // 更新時のパラメータ、新規作成時と項目が違ってもよい
  override def updateParams = Params(params).withTime("alert_time")

  // Model に渡す永続化すべき属性の一覧を定義します
  override def updateFormStrongParameters = Seq(
    "name" -> ParamType.String,
    "lucky_number" -> ParamType.Long,
    "alert_time" -> ParamType.LocalTime
  )

}

SkinnyResource の実装は Controller のサンプルとして一度目を通してみるとよいかと思います。一部だけ変更したい場合はその箇所を override して書き換えて対応してください。なお、SkinnyResource のうち、一部だけを使いたいという場合は SkinnyResourceActions だけを mixin してルーティングを自分で定義してください。

https://github.com/seratch/skinny-framework/blob/develop/framework/src/main/scala/skinny/controller/SkinnyResource.scala

フォーム、バリデーション

ここで出てきたバリデーションは Skinny Framework のオリジナルな実装です。skinny-validator というライブラリになっていて単体で利用が可能です(例えばこのバリデータを Play framework で利用する、といったことも可能)。

buit-in のバリデーションルールで多くのケースをまかなえるようになっていますが

https://github.com/seratch/skinny-framework/blob/develop/validator/src/main/scala/skinny/validator/BuiltinValidationRules.scala

built-in に必要なものがなかった場合やアプリケーション固有のバリデーションなどは以下のようにして実装します。

package lib.validation
// パラメータが必要な場合は case class にします
object notNull extends skinny.validator.ValidationRule {

  // 名前を指定、messages.conf のエラーメッセージとのひもづけに使用します
  def name = "notNull"

  // Any を受け取って Boolean を返すメソッドを定義します
  def isValid(v: Any) = v != null 
}

// Controller で使う
import _root_.lib.validation._
def form = validation(paramKey("name") is notNull)

エラーメッセージは src/main/resources/messages.conf を利用して表示します。i18n に対応しており Locale ごとのメッセージ一覧は messages_{locale}.conf という名前でファイルを作ってください。

https://github.com/seratch/skinny-framework/blob/develop/example/src/main/resources/messages_ja.conf

XML、JSON API の自動生成

SkinnyResource は JSON、XML のレスポンスを自動で生成してくれます。members の場合は

  • /members.xml
  • /members.json
  • /members/{memberId}.xml
  • /members/{memberId}.json

のような URL で利用できます。Skinny Framework では標準で json4s が利用可能になっています。詳細は json4s のドキュメントを参照してください。

https://github.com/json4s/json4s

もし、この JSON、XML レスポンスを提供したくない場合は respondTo を override して変更してください。

CoffeeScript、LESS、Sass、React JSX、Scala.js のサポート

src/main/webapp/WEB-INF/assets 配下には coffee、less のようなディレクトリがあります。その名の通り、ここには CoffeeScript、LESS などのソースコードを配置します。blank-app はデフォルトで設定済ですが AssetsController が mount されていれば、coffee/echo.coffeehttp://localhost:8080/assets/js/echo.js で常に最新版のコードがコンパイルされた JavaScript コードにアクセス可能です。LESS の方は less/box.less のコンパイル結果が http://localhost:8080/assets/css/box.css でアクセスできます。

skinny-blank-app には /WEB-INF/assets/coffee/echo.coffee というサンプルが含まれています。

echo = (v) -> console.log v

http://localhost:8080/assets/js/echo.js にアクセスすると以下のように JS に展開されます。

(function() {
  var echo;

  echo = function(v) {
    return console.log(v);
  };

}).call(this);

AssetsController は適切に Last-Modified ヘッダを返し 304 応答もするように実装されていますが、本番運用では skinny package で js/css ファイルに変換した静的ファイルでアクセスを受けるのがよいかと思います。

Skinny ORM

次に Skinny ORM について説明します。Skinny ORM は ScalikeJDBC をベースにしており ActiveRecord に強く影響された ORM です。

https://github.com/seratch/scalikejdbc

members というテーブルがあってその model を定義する場合は以下のようになります。scaffold でもこのようなコードが生成されているはずです。

// エンティティ、Plain Old Scala Object で定義可能
case class Member(
  name: String,
  luckyNumber: Option[Long] = None,
  createdAt: DateTime,
  updatedAt: Option[DateTime] = None)

// ActiveRecord 的な API を提供するコンパニオンオブジェクト
object Member extends SkinnyCRUDMapper[Member] with TimestampsFeature[Member] {
  override val tableName = "members"
  override val defaultAlias = createAlias("m")

  override def extract(rs: WrappedResultSet, rn: ResultName[Member]): Member = new Member(
    id = rs.get(rn.id),
    name = rs.get(rn.name),
    luckyNumber = rs.get(rn.luckyNumber),
    createdAt = rs.get(rn.createdAt),
    updatedAt = rs.get(rn.updatedAt)
  )
}

この Member はこれだけ定義しておけば以下のようなメソッドが利用できます。

val id = Member.createWithAttributes('name -> "Alice", 'luckyNumber -> Some(777))
Member.updateById(id).withAttribute('name -> "Bob")
val bob: Option[Member] = Member.findById(id)

val members: List[Member] = Member.where('name -> "Bob").limit(10).offset(0).apply()
Member.deleteById(id)

SkinnyRecord という trait を case class の方に mixin すると以下のように ActiveRecord のオブジェクトらしくふるまうこともできます。

case class Company(id: Option[Long] = None, name: String) extends MutableSkinnyRecord[Company] {
  def skinnyCRUDMapper = Company
}
case class Place(id: Long, name: String) extends SkinnyRecord[Country] {
  def skinnyCRUDMapper = Country
}

val company = Company("Sun").create()
company.copy(name = "Oracle").save()
company.destroy()

val place = Place.createWithAttributes('name -> "Aki")
place.copy(name = "Hiroshima").save()
place.destroy()

他のテーブルと結合するには belongsTo、hasOne、hasMany、hasManyThrough を定義します。

case class Member(id: Long, name: String, 
  companyId: Option[Long] = None, 
  company: Option[Company] = None,
  skills: Seq[Skill] = Nil)

object Member extends SkinnyCRUDMapper[Member] {
  // byDefault にすると常に join される
  belongsTo[Company](Company, (m, c) => m.copy(company = c)).byDefault
  // こちらは joins で指定した時だけ join される
  val skills = hasManyThrough[Skill](
    MemberSkill, Skill, (member, ss) => member.copy(skills = ss))
}

// company も含まれる
val member = Member.findById(123)
// company も skills も含まれる
val member = Member.joins(Member.skills).findById(123)

その他、対応済のユースケースの確認にはテストコードが参考になると思います。

https://github.com/seratch/skinny-framework/blob/develop/orm/src/test/scala/skinny/orm/models.scala

https://github.com/seratch/skinny-framework/blob/develop/orm/src/test/scala/skinny/orm/SkinnyORMSpec.scala

ORM の実装はそれなりに大きな仕事なので、まだ未対応のケースや機能があるかもしれません。Skinny ORM に興味を持っていただけた方はぜひご要望やフィードバックを GitHub の issue までお寄せください。

DB マイグレーション

Java で実装された DB マイグレーションツールである Flyway をベースにした DB マイグレーションツールを提供しています。

http://flywaydb.org/

ルールはシンプルで src/main/resources/db/migration の下に Flyway のルールに従って SQL ファイルを置くだけです。

development/qa/production で別のファイルを使いたい、複数の DB のマイグレーションをしたいという場合は application.conf で {env}.db.{dbname}.migration.locations を指定してください。以下の場合だと src/main/resources/db/migration/development/default 配下が scan されます。

development {
  db {
    default {
      driver="org.h2.Driver"
      url="jdbc:h2:file:db/example-development"
      user="sa"
      password="sa"
      migration {
        locations: ["development.default"]
      }
    }
  }
}

default でない DB は以下のように設定して ./skinny db:migrate development yetanother のように実行します。

development {
  db {
    yetanother {
      driver="org.h2.Driver"
      url="jdbc:h2:file:db/yetanother"
      user="sa"
      password="sa"
      migration {
        locations: ["development.yetanother"]
      }
    }
  }
}

タスクを実行する

skinny コマンドは内部では skinny-task で提供される scaffold タスクなどを実行しています。タスクは簡単に追加する事ができます。task というディレクトリにある TaskLauncher.scala を見てください。例えば assets:precompile は以下のように定義されています。

object TaskLancher extends skinny.task.TaskLauncher {
  // def register(name: String, runner: (List[String]) => Unit)
  register("assets:precompile", (params) => skinny.task.AssetsPrecompileTask.main(Array("build")))
}

たとえば、単純なタスクはこのようにして追加できます。

register("echo", (params) => params foreach println)

sbt task/run で実行できます。

sbt "task/run echo foo bar baz"

[info] Running TaskLancher echo foo bar baz
foo
bar
baz

ただ関数を定義するだけなので、ファイルの生成だけでなく、何でもやることができます。工夫してよりよい開発環境をつくってみてください。よいアイデアが思いついたら、ぜひブログ記事や Pull request で共有していただければと思います。

テストを書く

Skinny framework はテストのしやすさをかなり重視しています。元々 Scalatra はテストの書きやすいフレームワークですが、SKinny が提供するそれ以外の部分(ORM もバリデータなど)についてもテストは書きやすい環境が整っています。

scaffold でテストコードが生成されるので controller のテストはそちらを見るのが手っ取り早いと思います。

Scalatra のテストサポートに不足している点として session の中身をテストコードから簡単に注入する機能がないので SkinnyTestSupport という trait で提供しています。ログイン状態をつくったり、セッションベースの CSRF トークンなどを簡単に設定してテストを書くことができます。

withSession("csrf-token" -> "aaaaaa") {
  delete(s"/members/${member.id}?csrf-token=aaaaaa") {
    status should equal(200)
  }
}

また Skinny ORM には Ruby の factory_girl のような扱いやすいフィクスチャ生成ツールがあります。名前はそのまま FactoryGirl です。

src/test/resources/factories.conf というファイルを用意して

company {
  name="Typesafe"
}

以下のように呼び出すと name に "Typesafe" と設定されたデータが DB に insert されてさらにインスタンスも取得できます。

val comapny = FactoryGirl(Company).create()

特定の属性だけ変更することもできます。

val google = FactoryGirl(Company).create('name -> "Google")

その他の使い方はテストコードを参照してください。

https://github.com/seratch/skinny-framework/blob/develop/orm/src/test/scala/skinny/orm/SkinnyORMSpec.scala

デプロイするための war ファイルをつくる

./skinny package で war ファイルを生成します。war ファイルを作る前に CoffeeScript、LESS などは JS、CSS にコンパイルされて /assets/ 配下に配置し Scalate テンプレートを全て precompile します。

開発時は Scalate のテンプレートはアクセスされてないもののエラーは検知されませんでしたが、どこかに壊れた箇所があれば package タスクの失敗という形で検知されます。

[info] Compiling 21 Scala sources to /.../build/target/scala-2.10/classes...
[info] Packaging /.../build/target/scala-2.10/skinny-blank-app_2.10-0.0.1-SNAPSHOT.war ...
[info] Done packaging.

これから

多くの方々のフィードバックと貢献をいただき、1.0 をリリースすることが出来ました。その後も細かい改善を続けており、できるだけスピーディなリリースを心がけて開発を続けています。

提案やご要望、感想でも結構ですのでお気軽にフィードバックをいただければ幸いです。ユーザの皆さんと Skinny Framework を育てていきたいと思います。

http://git.io/skinny

Enjoy Skinny Framework!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment