Riot.js/riot_js-railsの手直し のバックアップ(No.8)

更新


公開メモ

概要

rails から riot.js を使うための gem として、 riot_js-rails というのがあるのですが、 環境によってどうもうまく動かないように思えたため、手直しできないか調べてみました。

結果、どうにも手に負える物ではなかった感じです。。。

症状

$ ruby --version
 ruby 1.9.3p551 (2014-11-13 revision 48407) [x86_64-linux]
$ rails new riot_js-rails_sample -B
$ cd riot_js-rails_sample/
$ jed Gemfile
 gem 'riot_js-rails'
$ bundle install
$ cat Gemfile.lock
 ...
 DEPENDENCIES
   coffee-rails (~> 4.0.0)
   jbuilder (~> 1.2)
   jquery-rails
   rails (= 4.0.13)
   riot_js-rails
   sass-rails (~> 4.0.2)
   sdoc
   sqlite3
   turbolinks
   uglifier (>= 1.3.0)

 BUNDLED WITH
    1.11.2
$ rails generate controller main index
       create  app/controllers/main_controller.rb
        route  get "main/index"
       invoke  erb
       create    app/views/main
       create    app/views/main/index.html.erb
       invoke  test_unit
       create    test/controllers/main_controller_test.rb
       invoke  helper
       create    app/helpers/main_helper.rb
       invoke    test_unit
       create      test/helpers/main_helper_test.rb
       invoke  assets
       invoke    coffee
       create      app/assets/javascripts/main.js.coffee
       invoke    scss
       create      app/assets/stylesheets/main.css.scss
$ jed config/routes.rb
    root 'main#index'
$ rails s &
$ wget -q -O - http://localhost:3000/
 ...
 <h1>Main#index</h1>
 <p>Find me in app/views/main/index.html.erb</p>
 ...
$ cat > app/assets/javascripts/test-tag.tag
 <test-tag>
   Ok!
 </test-tag>
$ jed app/assets/javascripts/application.js
+  //= require riot
+  //= require riot-rails
   //= require_tree .
$ wget -q -O - http://localhost:3000/
 <script data-turbolinks-track="true" src="/assets/riot.js?body=1"></script>
 <script data-turbolinks-track="true" src="/assets/riot_rails.js?body=1"></script>
 <script data-turbolinks-track="true" src="/assets/main.js?body=1"></script>
 <script data-turbolinks-track="true" src="/assets/application.js?body=1"></script>

require_tree で test-tag.tag が読み込まれていません。

$ mv app/assets/javascripts/test-tag.tag app/assets/javascripts/test-tag.js.tag
$ wget -q -O - http://localhost:3000/
 <script data-turbolinks-track="true" src="/assets/riot.js?body=1"></script>
 <script data-turbolinks-track="true" src="/assets/riot_rails.js?body=1"></script>
 <script data-turbolinks-track="true" src="/assets/main.js?body=1"></script>
 <script data-turbolinks-track="true" src="/javascripts/test-tag.js.tag.js"></script>
 <script data-turbolinks-track="true" src="/assets/application.js?body=1"></script>
$ wget -q -O - http://localhost:3000/javascripts/test-tag.js.tag.js
 Started GET "/javascripts/test-tag.js.tag.js" for 127.0.0.1 at 2016-07-01 13:55:32 +0900
 ActionController::RoutingError (No route matches [GET] "/javascripts/test-tag.js.tag.js"):
$ wget -q -O - http://localhost:3000/assets/test-tag.js.tag.js?body=1
 Started GET "/assets/test-tag.js.tag.js?body=1" for 127.0.0.1 at 2016-07-01 13:56:24 +0900
 ActionController::RoutingError (No route matches [GET] "/assets/test-tag.js.tag.js"):

test-tag.tag ではなく test-tag.js.tag としたところ、読み込もうとする姿勢は見られましたが、 /assets/ ではなく /javascripts/ が指定されており、なおかつ両方とも RoutingError になってしまいます。

Rails のバージョンを上げると

$ rvm use ruby-2.2.1
$ ruby --version
 ruby 2.2.1p85 (2015-02-26 revision 49769) [x86_64-linux]
$ rails --version
 Rails 4.2.6
$ rails new riot_js-rails_sample -B
$ cd riot_js-rails_sample/
$ jed Gemfile
 gem 'riot_js-rails'
$ bundle install
$ cat Gemfile.lock
 DEPENDENCIES
   byebug
   coffee-rails (~> 4.1.0)
   jbuilder (~> 2.0)
   jquery-rails
   rails (= 4.2.6)
   riot_js-rails
   sass-rails (~> 5.0)
   sdoc (~> 0.4.0)
   spring
   sqlite3
   turbolinks
   uglifier (>= 1.3.0)
   web-console (~> 2.0)
$ echo "Sprockets::VERSION" | rails c
 Loading development environment (Rails 4.2.6)
 Switch to inspect mode.
 Sprockets::VERSION
 "3.6.3"

この環境だと、上と同じ操作で

 <script src="/assets/riot.self-d4789eae993b10e05f4694693fa33efe6afd5ccd866d3f6eee21de793efc7f31.js?body=1" data-turbolinks-track="true"></script>
 <script src="/assets/riot_rails.self-23d22569edc9e83fe5a74dbf647822c59a2fcd08777f496b38cab2507f2d594d.js?body=1" data-turbolinks-track="true"></script>
 <script src="/assets/main.self-877aef30ae1b040ab8a3aba4e3e309a11d7f2612f44dde450b5c157aa5f95c05.js?body=1" data-turbolinks-track="true"></script>
 <script src="/assets/test-tag.self-198db509a34ddfb1d978f239761117f57e5680e43ffe43f5524092ecb41ca2b3.js?body=1" data-turbolinks-track="true"></script>
 <script src="/assets/application.self-7862a8a8b42407b4741a1adeeea35f0d13ddc4f702ec532adb0674491d296495.js?body=1" data-turbolinks-track="true"></script>

のように、"test-tag.self-XXXX.js" が正しくコンパイルされます。

どうやら sprockets のバージョン依存・・・なのでしょうか?

ソースを確認

$ cd ..
$ git clone https://github.com/bjarosze/riot_js-rails.git
 Cloning into 'riot_js-rails'...
 remote: Counting objects: 180, done.
 remote: Total 180 (delta 0), reused 0 (delta 0), pack-reused 180
 Receiving objects: 100% (180/180), 79.78 KiB | 90.00 KiB/s, done.
 Resolving deltas: 100% (61/61), done.
 Checking connectivity... done.
$ cd riot_js-rails
$ ls -a
 .  ..  .git  .gitignore  Gemfile  README.md  Rakefile  lib  riot_js-rails.gemspec  test  vendor
$ git branch -m fix-asset-path
$ git status
 On branch fix-asset-path
 Your branch is up-to-date with 'origin/master'.
 nothing to commit, working directory clean
$ cat lib/riot_js/rails/railtie.rb
 require 'rails/railtie'
 require 'riot_js/rails/processors/processor'
 require 'riot_js/rails/helper'
 
 module RiotJs
   module Rails
     class Railtie < ::Rails::Railtie
 
       initializer :setup_sprockets do |app|
         Processor.register_self config
         ...
$ cat lib/riot_js/rails/processors/processor.rb
 require 'riot_js/rails/processors/compiler'
 
 if Gem::Version.new(Sprockets::VERSION) < Gem::Version.new('3.0.0')
   require 'riot_js/rails/processors/sprockets_processor_v2'
 else
   require 'riot_js/rails/processors/sprockets_processor_v3'
 end
 
 module RiotJs
   module Rails
     class Processor < SprocketsProcessor
 
       def process
         compile_tag
       end
 
       private
 
       def compile_tag
         ::RiotJs::Rails::Compiler.compile(@data)
       end
     end
   end
 end
$ echo "Sprockets::VERSION" | rails c
 Loading development environment (Rails 4.0.13)
 Switch to inspect mode.
 Sprockets::VERSION
 "2.12.4"
$ cat lib/riot_js/rails/processors/sprockets_rocessor_v2.rb
 module RiotJs
   module Rails
     class SprocketsProcessor < Tilt::Template
 
       self.default_mime_type = 'application/javascript'
 
       def self.register_self(app)
         app.assets.register_engine '.tag', self
       end
 
       def evaluate(context, locals, &block)
         @context = context
         process
       end
 
       def prepare
         @data = data
       end
 
       def process
         raise 'Not implemented'
       end
 
     end
   end
 end

これを直すのは難しそうです。。。

coffee-rails のソースを見る

特定の拡張子を持ったファイルを .js に変換するという動作は coffee-rails と同様なので、

https://github.com/rails/coffee-rails/

これと同じことをやれば良いはずなのですが、、、

.coffee から .js へのコンパイル部分は

https://github.com/rails/coffee-rails/blob/master/lib/coffee/rails/template_handler.rb

LANG:ruby
module Coffee
  module Rails
    class TemplateHandler
      def self.erb_handler
        @@erb_handler ||= ActionView::Template.registered_template_handler(:erb)
      end

      def self.call(template)
        compiled_source = erb_handler.call(template)
        "CoffeeScript.compile(begin;#{compiled_source};end)"
      end
    end
  end
end

ActiveSupport.on_load(:action_view) do
  ActionView::Template.register_template_handler :coffee, Coffee::Rails::TemplateHandler
end

だけだと思われて、なおかつこの部分は上記前者で読まれた v4.0.1 でも、 後者で読まれた v4.1.1 でも変更されていないにも関わらず、 同じことをしてみても、どうもうまくいかず、困ってしまいます。

coffee-rails のまねをする

$ git clone https://github.com/rails/coffee-rails.git
$ mv coffee-rails/ riottag-rails
$ (いろいろ直す)

主には、

LANG:ruby
module RiotTag
  module Rails
    class TemplateHandler
      def self.erb_handler
        @@erb_handler ||= ActionView::Template.registered_template_handler(:erb)
      end

      def self.call(template)
        compiled_source = erb_handler.call(template)
        "'Compiled!'.html_safe"
      end
    end
  end
end

ActiveSupport.on_load(:action_view) do
  ActionView::Template.register_template_handler :tag, RiotTag::Rails::TemplateHandler
end

全然うまくいきません(涙

というか、coffee-rails の該当部分 ActionView::Template.register_template_handler をコメントアウトしても coffee-rails が動き続けているあたり、見ている場所が全然違うのかもしれません?!

素直に rails のバージョンを上げることを考えるべきなのかも・・・

それにしても黒魔術すぎて、キビシイなあ

それっぽい記事を発見(未検証)

Supporting All Versions of Sprockets in Processors
https://github.com/rails/sprockets/blob/master/guides/extending_sprockets.md#supporting-all-versions-of-sprockets-in-processors

これによると、

LANG:ruby
class SprocketsExtensionBase
  def initialize(filename, &block)
    @filename = filename
    @source   = block.call
  end

  def render(context, empty_hash_wtf)
    self.class.run(@filename, @source, context)
  end

  def self.run(filename, source, context)
    # return the modified source
  end

  def self.call(input)
    filename = input[:filename]
    source   = input[:data]
    context  = input[:environment].context_class.new(input)
 
    result = run(filename, source, context)
    context.metadata.merge(data: result)
  end
end

を継承して run を override すれば Extention を作れる。

LANG:ruby
require 'sprockets/processing'
extend Sprockets::Processing

register_preprocessor 'text/css', TheSprocketsExtension

で拡張機能を登録して、

LANG:ruby
def register_sprockets_extention(env, sprockets_extention, mime_type, file_ext, charset=nil)
  file_ext = [file_ext] unless file_ext.instance_of?(Array)
  charset = charset.to_sym
 
  # Sprockets 2, 3, and 4
  
  if env.respond_to?(:register_transformer)
    env.register_mime_type mime_type, extensions: file_ext, charset: charset
    env.register_preprocessor mime_type, sprockets_extention
  end
  
  if env.respond_to?(:register_engine)
    args = [file_ext, sprockets_extention]
    args << { mime_type: mime_type, silence_deprecation: true } if Sprockets::VERSION.start_with?("3")
    env.register_engine(*args)
  end
end

これで良さそう?

あら、ものすごく重要なことがさらっと書いてあった。

prockets 4 will not chain asset extensions so .coffee.erb is explicitly registered in addition to .coffee. If your application introduces a new mime/extension combo it will be responsible for registering all combinations.

だから rails 5 で file.tag.haml が動かなかったのか?
いや、rails 5.0.0.1 でも sprockets は 3.7 だから、そういう問題ではないか。

LANG:ruby
if Sprockets::VERSION.start_with?("4")
  register_sprockets_extention env, 'application/javascript', '.tag.haml', nil,
              Proc.new {|input| TagProcessor.call(HAMLProcessor.call(input)) }
end

こんな感じかな?

現行のコードと比べてみる

riot_js-rails/lib/riot_js/rails/processors/sprockets_processor_v2.rb

LANG:ruby
module RiotJs
  module Rails
    class SprocketsProcessor < Tilt::Template

      self.default_mime_type = 'application/javascript'

      def self.register_self(app)
        app.assets.register_engine '.tag', self
      end

      def evaluate(context, locals, &block)
        @context = context
        process
      end
      
      def prepare
        @data = data
      end

      def process
        raise 'Not implemented'
      end

    end
  end
end


riot_js-rails/lib/riot_js/rails/processors/sprockets_processor_v3.rb

LANG:ruby
module RiotJs
  module Rails
    class SprocketsProcessor
      def self.instance
        @instance ||= new
      end

      def self.call(input)
        instance.call(input)
      end

      def call(input)
        prepare(input)
        data = process

        @context.metadata.merge(data: data)
      end

      def self.register_self(config)
        config.assets.configure do |env|
          env.register_engine '.tag', self, mime_type: 'application/javascript'
        end
      end
 
     private

     def process
        raise 'Not implemented'
      end

      def prepare(input)
        @context = input[:environment].context_class.new(input)
        @data = input[:data]
      end

    end
  end
end


riot_js-rails/lib/riot_js/rails/processors/processor.rb

LANG:ruby
require 'riot_js/rails/processors/compiler'

if Gem::Version.new(Sprockets::VERSION) < Gem::Version.new('3.0.0')
  require 'riot_js/rails/processors/sprockets_processor_v2'
else
  require 'riot_js/rails/processors/sprockets_processor_v3'
end

module RiotJs
  module Rails
    class Processor < SprocketsProcessor

      def process
        compile_tag
      end

      private

      def compile_tag
        ::RiotJs::Rails::Compiler.compile(@data)
      end
    end
  end
end

いろいろ面倒な手順は踏んでいるけれど、やろうとしていることは同じように見える?

やってみる

あー、development 環境でも assets のキャッシュのクリアはひと手間必要で、 ちゃんとクリアしないと古い内容が読み込まれてしまうのが第一の問題だったっぽい。

LANG:console
$ rake tmp:cache:clear
$ rake assets:clean

でクリアできた。

これがないとまったく始まらない(汗

で、下記コードで sprocket 3.7 ではうまくいった。

LANG:ruby
require 'riot_js/rails/processors/compiler'

module RiotJs
  module Rails

    # Sprockets 2, 3 & 4 interface
    class SprocketsExtensionBase
      def initialize(filename, &block)
        @filename = filename
        @source   = block.call
      end

      def render(context, empty_hash_wtf)
        self.class.run(@filename, @source, context)
      end

      def self.run(filename, source, context)
        raise 'Not implemented'
      end

      def self.call(input)
        filename = input[:filename]
        source   = input[:data]
        context  = input[:environment].context_class.new(input)

        result = run(filename, source, context)
        context.metadata.merge(data: result)
      end

      private

      def self.register_self_helper(config, file_ext, mime_type_from, mime_type_to, charset=nil)
        config.assets.configure do |env|
          # Sprockets 3 and 4
          if env.respond_to?(:register_transformer)
            env.register_mime_type mime_type_from, extensions: [file_ext], charset: charset
            env.register_transformer mime_type_from, mime_type_to, self
          elsif env.respond_to?(:register_engine)
            # Sprockets 2 and 3
            if Sprockets::VERSION.start_with?("3")
              env.register_engine file_ext_from, self, { mime_type: mime_type, silence_deprecation: true }
            else
              env.register_engine file_ext_from, self, mime_type: mime_type
            end
          end
        end
      end

    end

    class Processor < SprocketsExtensionBase

      def self.run(filename, source, context)
        ::RiotJs::Rails::Compiler.compile(source)
      end

      def self.register_self(env)
        register_self_helper(env, '.tag', 'text/riot-tag', 'application/javascript', :html)
      end

    end
  end
end


LANG:ruby
require 'rails/railtie'
require 'riot_js/rails/processors/processor'
require 'riot_js/rails/helper'

module RiotJs
  module Rails
    class Railtie < ::Rails::Railtie
      config.riot = ActiveSupport::OrderedOptions.new
      config.riot.node_paths = []

      initializer :setup_sprockets do |app|

        Processor.register_self config

        if defined?(::Haml)
          require 'tilt/haml'
          Haml::Template.options[:format] = :html5

          config.assets.configure do |env|
            # Sprockets 3 and 4
            if env.respond_to?(:register_transformer)
              env.register_mime_type 'text/riot-tag+haml', extensions: ['.tag.haml'], charset: :html
              env.register_transformer 'text/riot-tag+haml', 'application/javascript', Proc.new{ |input|
                Processor.call(::Haml::Rails::Processor.new(input).render)
              }
            elsif env.respond_to?(:register_engine)
              # Sprockets 2 and 3
              if Sprockets::VERSION.start_with?("3")
                env.register_engine '.haml', ::Tilt::HamlTemplate, { silence_deprecation: true }
              else
                env.register_engine '.haml', ::Tilt::HamlTemplate
              end
            end
          end
        end
      end

      initializer :add_helpers do |app|
        helpers = %q{ include ::RiotJs::Rails::Helper }
        ::ActionView::Base.module_eval(helpers)
        ::Rails.application.config.assets.context_class.class_eval(helpers)
      end

      config.after_initialize do |app|
        node_paths = ENV['NODE_PATH'].to_s.split(':')
        node_paths += app.config.riot.node_paths
        node_global_path = detect_node_global_path
        node_paths << node_global_path if node_global_path
        
        ENV['NODE_PATH'] = node_paths.join(':')
      end

      def detect_node_global_path
        prefix = `npm config get prefix`.to_s.chomp("\n")
        possible_paths = [ "#{prefix}/lib/node", "#{prefix}/lib/node_modules" ]

        possible_paths.each do |path|
          return path if File.directory?(path)
        end
        return
      end

    end
  end
end

Counter: 7594 (from 2010/06/03), today: 1, yesterday: 0