Webアプリのモックアップ作業土台を作る その3 Sass Source Map

その1その2 Sprockets の続きです。が、今回の作業は その1 の途中からブランチしてます。

Google Chrome の Dev Tools は Sass/SCSS の Source Map に対応しているので利用できるようにしてみます。が、Source Map は仕様も実装も流動的な状態で、環境によって動作しない可能性も大ありなので、あくまで参考まで。

リポジトリ上では Sprockets も Bootstrap も Compass も導入する前の段階からブランチを生やしました。

新仕様と旧仕様

CSS リクエストへのレスポンスでブラウザに Source Map の位置(パス)を知らせる必要があり、その方法は2種類あります。

  1. レスポンスの HTTP ヘッダで渡す
  2. ボディ内のコメントで渡す
HTTP ヘッダ(新)
SourceMap:
HTTP ヘッダ(旧)
X-SourceMap:
コメント(新)
/*# sourceMappingURL=path/to/sourcemap */
コメント(旧)
/*@ sourceMappingURL=

手元の Chrome (27.0.1453.116 Mac stable channel) は HTTP ヘッダを見てくれず、コメントのほうは旧仕様にしか対応していない、という状況なので、ライブラリや出力はそれに合わせています。Beta/Dev Channel では新仕様に対応しているかもしれません(未確認)。

Sass gem の対応状況

Ruby の sass gem は prerelease (alpha) 版だけが Source Map に対応していますが、ここでは特に sass 3.3.0.alpha.136 を決め打ちで利用。

現在の最新は 3.3.0.alpha.198 ですが、198では前段のコメント出力が新仕様になっています。

まずは Gemfile に追加して bundle

gem 'sass', '3.3.0.alpha.136'

Sinatra Extension を書く

Sinatra Extension でヘルパーメソッドを追加(だいぶやっつけ感がありますが)。ソース全体は Github で参照してもらうことにして、ここではポイントだけ。

https://github.com/cu39/workbench/blob/sass-sourcemap-on-plain/lib/sinatra/sass_sourcemap.rb

旧仕様と新仕様の HTTP ヘッダを出力するヘルパー(JS用のパスを入れたヘッダと混在するとどうなるのかよくわかってない)。

def sass_map_header(template, opts = {}, locals = {})
  sourcemap_path_info = request.path_info.sub(/\.css$/, '.sassmap')
  response['X-SourceMap'] = sourcemap_path_info
  response['SourceMap'] = sourcemap_path_info
end

デフォルトの sass の代替となるヘルパー。ブラウザが前段の HTTP ヘッダに対応してくれれば必要なくなるもの。

def sass_with_map(template, opts = {}, locals = {})
  content_type :css
  css, srcmap = __sass_render(template, opts, locals)
  @output = css
end

Source Map を出力するヘルパー。*.sass ソースへのパスが相対パスに変換されてしまうので file:// スキームに置換しています。

def sass_map(template, opts = {}, locals = {})
  content_type :json
  css, srcmap = __sass_render(template, opts, locals)
  css_path_info = request.path_info.sub(/\.sassmap$/, '.css')
  json_opts = { css_path: css_path_info, sourcemap_path: request.path_info }
  json = JSON srcmap.to_json(json_opts)
  json['sources'].each do |src_path| src_path.sub!(/^\.\./, 'file://') end
  @output = json.to_json
end

sass → css をコンパイルするメソッド。Sinatra では Tilt::Template を使ってますが、ここでは直接 Sass::Engine インスタンスを作って #render_with_sourcemap を呼びます。

def __sass_render(template, opts = {}, locals = {})
  css_path_info = sassmap_path_info = request.path_info
  css_path_info.sub!(/\.sassmap$/, '.css')
  sassmap_path_info.sub!(/\.css$/, '.sassmap')
  sass_path = File.join(settings.views, template.to_s + '.sass')

  opts = __sass_merged_options(filename: sass_path)
  engine = ::Sass::Engine.new(File.read(sass_path), opts)
  engine.render_with_sourcemap(sassmap_path_info)
end

Sinatra アプリ修正

https://github.com/cu39/workbench/blob/sass-sourcemap-on-plain/workbench.rb

Extension を読み込んで

require 'sinatra/sass_sourcemap'

ヘルパーを追加して

  configure :development, :test do
    register Sinatra::Reloader
    helpers Sinatra::SassSourcemap
  end

CSS レスポンスを置き換えて

  get '/css/application.css' do
    sass_map_header :application
    sass_with_map :application
  end

Source Map レスポンスも設定して

  get '/css/application.sassmap' do
    halt 404, "Not Found\n" if settings.production?
    sass_map :application
  end

Rack Up ファイルで $LOAD_PATHlib を追加すれば完成。

config.ru

require 'bundler'
Bundler.setup

$LOAD_PATH << File.expand_path(File.join('..','lib'), __FILE__)
require File.expand_path(File.join('..','workbench'), __FILE__)
require 'rack-livereload'