web-console 2.3.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.markdown +133 -1
  3. data/MIT-LICENSE +1 -1
  4. data/README.markdown +48 -86
  5. data/Rakefile +14 -12
  6. data/lib/web-console.rb +3 -1
  7. data/lib/web_console/context.rb +45 -0
  8. data/lib/web_console/errors.rb +2 -0
  9. data/lib/web_console/evaluator.rb +14 -5
  10. data/lib/web_console/exception_mapper.rb +56 -0
  11. data/lib/web_console/extensions.rb +28 -17
  12. data/lib/web_console/injector.rb +32 -0
  13. data/lib/web_console/interceptor.rb +18 -0
  14. data/lib/web_console/locales/en.yml +1 -1
  15. data/lib/web_console/middleware.rb +31 -31
  16. data/lib/web_console/permissions.rb +42 -0
  17. data/lib/web_console/railtie.rb +38 -24
  18. data/lib/web_console/request.rb +8 -20
  19. data/lib/web_console/session.rb +32 -18
  20. data/lib/web_console/source_location.rb +15 -0
  21. data/lib/web_console/tasks/extensions.rake +15 -13
  22. data/lib/web_console/tasks/templates.rake +56 -0
  23. data/lib/web_console/template.rb +4 -3
  24. data/lib/web_console/templates/console.js.erb +497 -37
  25. data/lib/web_console/templates/error_page.js.erb +7 -8
  26. data/lib/web_console/templates/index.html.erb +4 -0
  27. data/lib/web_console/templates/layouts/inlined_string.erb +1 -1
  28. data/lib/web_console/templates/layouts/javascript.erb +1 -1
  29. data/lib/web_console/templates/regular_page.js.erb +24 -0
  30. data/lib/web_console/templates/style.css.erb +182 -27
  31. data/lib/web_console/testing/erb_precompiler.rb +5 -3
  32. data/lib/web_console/testing/fake_middleware.rb +7 -10
  33. data/lib/web_console/testing/helper.rb +3 -1
  34. data/lib/web_console/version.rb +3 -1
  35. data/lib/web_console/view.rb +24 -3
  36. data/lib/web_console/whiny_request.rb +8 -6
  37. data/lib/web_console.rb +28 -20
  38. metadata +28 -63
  39. data/lib/web_console/helper.rb +0 -22
  40. data/lib/web_console/integration/cruby.rb +0 -40
  41. data/lib/web_console/integration/jruby.rb +0 -111
  42. data/lib/web_console/integration/rubinius.rb +0 -67
  43. data/lib/web_console/integration.rb +0 -8
  44. data/lib/web_console/response.rb +0 -23
  45. data/lib/web_console/tasks/test_templates.rake +0 -50
  46. data/lib/web_console/whitelist.rb +0 -42
@@ -1,23 +1,34 @@
1
- ActionDispatch::DebugExceptions.class_eval do
2
- def render_exception_with_web_console(request, exception)
3
- render_exception_without_web_console(request, exception).tap do
4
- # Retain superficial Rails 5 compatibility.
5
- env = Hash === request ? request : request.env
1
+ # frozen_string_literal: true
6
2
 
7
- error = ActionDispatch::ExceptionWrapper.new(env, exception).exception
3
+ module Kernel
4
+ module_function
8
5
 
9
- # Get the original exception if ExceptionWrapper decides to follow it.
10
- env['web_console.exception'] = error
6
+ # Instructs Web Console to render a console in the specified binding.
7
+ #
8
+ # If +binding+ isn't explicitly given it will default to the binding of the
9
+ # previous frame. E.g. the one that invoked +console+.
10
+ #
11
+ # Raises +DoubleRenderError+ if a more than one +console+ invocation per
12
+ # request is detected.
13
+ def console(binding = Bindex.current_bindings.second)
14
+ raise WebConsole::DoubleRenderError if Thread.current[:__web_console_binding]
11
15
 
12
- # ActionView::Template::Error bypass ExceptionWrapper original
13
- # exception following. The backtrace in the view is generated from
14
- # reaching out to original_exception in the view.
15
- if error.is_a?(ActionView::Template::Error)
16
- env['web_console.exception'] = error.original_exception
17
- end
18
- end
16
+ Thread.current[:__web_console_binding] = binding
17
+
18
+ # Make sure nothing is rendered from the view helper. Otherwise
19
+ # you're gonna see unexpected #<Binding:0x007fee4302b078> in the
20
+ # templates.
21
+ nil
19
22
  end
23
+ end
20
24
 
21
- alias_method :render_exception_without_web_console, :render_exception
22
- alias_method :render_exception, :render_exception_with_web_console
25
+ class Binding
26
+ # Instructs Web Console to render a console in the current binding, without
27
+ # the need to unroll the stack.
28
+ #
29
+ # Raises +DoubleRenderError+ if a more than one +console+ invocation per
30
+ # request is detected.
31
+ def console
32
+ Kernel.console(self)
33
+ end
23
34
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebConsole
4
+ # Injects content into a Rack body.
5
+ class Injector
6
+ def initialize(body, headers)
7
+ @body = "".dup
8
+
9
+ body.each { |part| @body << part }
10
+ body.close if body.respond_to?(:close)
11
+
12
+ @headers = headers
13
+ end
14
+
15
+ def inject(content)
16
+ # Set Content-Length header to the size of the current body
17
+ # + the extra content. Otherwise the response will be truncated.
18
+ if @headers["Content-Length"]
19
+ @headers["Content-Length"] = (@body.bytesize + content.bytesize).to_s
20
+ end
21
+
22
+ [
23
+ if position = @body.rindex("</body>")
24
+ [ @body.insert(position, content) ]
25
+ else
26
+ [ @body << content ]
27
+ end,
28
+ @headers
29
+ ]
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,18 @@
1
+ module WebConsole
2
+ module Interceptor
3
+ def self.call(request, exception)
4
+ backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner")
5
+ error = ActionDispatch::ExceptionWrapper.new(backtrace_cleaner, exception).exception
6
+
7
+ # Get the original exception if ExceptionWrapper decides to follow it.
8
+ Thread.current[:__web_console_exception] = error
9
+
10
+ # ActionView::Template::Error bypass ExceptionWrapper original
11
+ # exception following. The backtrace in the view is generated from
12
+ # reaching out to original_exception in the view.
13
+ if error.is_a?(ActionView::Template::Error)
14
+ Thread.current[:__web_console_exception] = error.cause
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,7 +1,7 @@
1
1
  en:
2
2
  errors:
3
3
  unavailable_session: |
4
- Session %{id} is is no longer available in memory.
4
+ Session %{id} is no longer available in memory.
5
5
 
6
6
  If you happen to run on a multi-process server (like Unicorn or Puma) the process
7
7
  this request hit doesn't store %{id} in memory. Consider turning the number of
@@ -1,14 +1,13 @@
1
- require 'active_support/core_ext/string/strip'
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/string/strip"
2
4
 
3
5
  module WebConsole
4
6
  class Middleware
5
- TEMPLATES_PATH = File.expand_path('../templates', __FILE__)
6
-
7
- cattr_accessor :mount_point
8
- @@mount_point = '/__web_console'
7
+ TEMPLATES_PATH = File.expand_path("../templates", __FILE__)
9
8
 
10
- cattr_accessor :whiny_requests
11
- @@whiny_requests = true
9
+ cattr_accessor :mount_point, default: "/__web_console"
10
+ cattr_accessor :whiny_requests, default: true
12
11
 
13
12
  def initialize(app)
14
13
  @app = app
@@ -17,7 +16,7 @@ module WebConsole
17
16
  def call(env)
18
17
  app_exception = catch :app_exception do
19
18
  request = create_regular_or_whiny_request(env)
20
- return @app.call(env) unless request.from_whitelited_ip?
19
+ return call_app(env) unless request.permitted?
21
20
 
22
21
  if id = id_for_repl_session_update(request)
23
22
  return update_repl_session(id, request)
@@ -25,49 +24,46 @@ module WebConsole
25
24
  return change_stack_trace(id, request)
26
25
  end
27
26
 
28
- status, headers, body = @app.call(env)
29
27
 
30
- if exception = env['web_console.exception']
31
- session = Session.from_exception(exception)
32
- elsif binding = env['web_console.binding']
33
- session = Session.from_binding(binding)
34
- end
28
+ status, headers, body = call_app(env)
35
29
 
36
- if session && acceptable_content_type?(headers)
37
- response = Response.new(body, status, headers)
38
- template = Template.new(env, session)
30
+ if (session = Session.from(Thread.current)) && acceptable_content_type?(headers)
31
+ headers["X-Web-Console-Session-Id"] = session.id
32
+ headers["X-Web-Console-Mount-Point"] = mount_point
39
33
 
40
- response.headers["X-Web-Console-Session-Id"] = session.id
41
- response.headers["X-Web-Console-Mount-Point"] = mount_point
42
- response.write(template.render('index'))
43
- response.finish
44
- else
45
- [ status, headers, body ]
34
+ template = Template.new(env, session)
35
+ body, headers = Injector.new(body, headers).inject(template.render("index"))
46
36
  end
37
+
38
+ [ status, headers, body ]
47
39
  end
48
40
  rescue => e
49
41
  WebConsole.logger.error("\n#{e.class}: #{e}\n\tfrom #{e.backtrace.join("\n\tfrom ")}")
50
42
  raise e
51
43
  ensure
44
+ # Clean up the fiber locals after the session creation. Object#console
45
+ # uses those to communicate the current binding or exception to the middleware.
46
+ Thread.current[:__web_console_exception] = nil
47
+ Thread.current[:__web_console_binding] = nil
48
+
52
49
  raise app_exception if Exception === app_exception
53
50
  end
54
51
 
55
52
  private
56
53
 
57
54
  def acceptable_content_type?(headers)
58
- Mime::Type.parse(headers['Content-Type']).first == Mime::HTML
55
+ headers["Content-Type"].to_s.include?("html")
59
56
  end
60
57
 
61
58
  def json_response(opts = {})
62
59
  status = opts.fetch(:status, 200)
63
- headers = { 'Content-Type' => 'application/json; charset = utf-8' }
60
+ headers = { "Content-Type" => "application/json; charset = utf-8" }
64
61
  body = yield.to_json
65
62
 
66
- Rack::Response.new(body, status, headers).finish
63
+ [ status, headers, [ body ] ]
67
64
  end
68
65
 
69
66
  def json_response_with_session(id, request, opts = {})
70
- return respond_with_unacceptable_request unless request.acceptable?
71
67
  return respond_with_unavailable_session(id) unless session = Session.find(id)
72
68
 
73
69
  json_response(opts) { yield session }
@@ -104,13 +100,17 @@ module WebConsole
104
100
 
105
101
  def update_repl_session(id, request)
106
102
  json_response_with_session(id, request) do |session|
107
- { output: session.eval(request.params[:input]) }
103
+ if input = request.params[:input]
104
+ { output: session.eval(input) }
105
+ elsif input = request.params[:context]
106
+ { context: session.context(input) }
107
+ end
108
108
  end
109
109
  end
110
110
 
111
111
  def change_stack_trace(id, request)
112
112
  json_response_with_session(id, request) do |session|
113
- session.switch_binding_to(request.params[:frame_id])
113
+ session.switch_binding_to(request.params[:frame_id], request.params[:exception_object_id])
114
114
 
115
115
  { ok: true }
116
116
  end
@@ -118,13 +118,13 @@ module WebConsole
118
118
 
119
119
  def respond_with_unavailable_session(id)
120
120
  json_response(status: 404) do
121
- { output: format(I18n.t('errors.unavailable_session'), id: id)}
121
+ { output: format(I18n.t("errors.unavailable_session"), id: id) }
122
122
  end
123
123
  end
124
124
 
125
125
  def respond_with_unacceptable_request
126
126
  json_response(status: 406) do
127
- { output: I18n.t('errors.unacceptable_request') }
127
+ { output: I18n.t("errors.unacceptable_request") }
128
128
  end
129
129
  end
130
130
 
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ipaddr"
4
+
5
+ module WebConsole
6
+ class Permissions
7
+ # IPv4 and IPv6 localhost should be always allowed.
8
+ ALWAYS_PERMITTED_NETWORKS = %w( 127.0.0.0/8 ::1 )
9
+
10
+ def initialize(networks = nil)
11
+ @networks = normalize_networks(networks).map(&method(:coerce_network_to_ipaddr)).uniq
12
+ end
13
+
14
+ def include?(network)
15
+ @networks.any? { |permission| permission.include?(network.to_s) }
16
+ rescue IPAddr::InvalidAddressError
17
+ false
18
+ end
19
+
20
+ def to_s
21
+ @networks.map(&method(:human_readable_ipaddr)).join(", ")
22
+ end
23
+
24
+ private
25
+
26
+ def normalize_networks(networks)
27
+ Array(networks).concat(ALWAYS_PERMITTED_NETWORKS)
28
+ end
29
+
30
+ def coerce_network_to_ipaddr(network)
31
+ if network.is_a?(IPAddr)
32
+ network
33
+ else
34
+ IPAddr.new(network)
35
+ end
36
+ end
37
+
38
+ def human_readable_ipaddr(ipaddr)
39
+ ipaddr.to_range.to_s.split("..").uniq.join("/")
40
+ end
41
+ end
42
+ end
@@ -1,32 +1,28 @@
1
- require 'rails/railtie'
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/railtie"
2
4
 
3
5
  module WebConsole
4
6
  class Railtie < ::Rails::Railtie
5
7
  config.web_console = ActiveSupport::OrderedOptions.new
6
- config.web_console.whitelisted_ips = %w( 127.0.0.1 ::1 )
7
-
8
- initializer 'web_console.initialize' do
9
- require 'web_console/extensions'
10
8
 
11
- ActiveSupport.on_load(:action_view) do
12
- ActionView::Base.send(:include, Helper)
13
- end
9
+ initializer "web_console.initialize" do
10
+ require "bindex"
11
+ require "web_console/extensions"
14
12
 
15
- ActiveSupport.on_load(:action_controller) do
16
- ActionController::Base.send(:include, Helper)
17
- end
13
+ ActionDispatch::DebugExceptions.register_interceptor(Interceptor)
18
14
  end
19
15
 
20
- initializer 'web_console.development_only' do
16
+ initializer "web_console.development_only" do
21
17
  unless (config.web_console.development_only == false) || Rails.env.development?
22
18
  abort <<-END.strip_heredoc
23
- Web Console is activated in the #{Rails.env} environment, which is
19
+ Web Console is activated in the #{Rails.env} environment. This is
24
20
  usually a mistake. To ensure it's only activated in development
25
21
  mode, move it to the development group of your Gemfile:
26
22
 
27
23
  gem 'web-console', group: :development
28
24
 
29
- If you still want to run it the #{Rails.env} environment (and know
25
+ If you still want to run it in the #{Rails.env} environment (and know
30
26
  what you are doing), put this in your Rails application
31
27
  configuration:
32
28
 
@@ -35,36 +31,54 @@ module WebConsole
35
31
  end
36
32
  end
37
33
 
38
- initializer 'web_console.insert_middleware' do |app|
34
+ initializer "web_console.insert_middleware" do |app|
39
35
  app.middleware.insert_before ActionDispatch::DebugExceptions, Middleware
40
36
  end
41
37
 
42
- initializer 'web_console.mount_point' do
38
+ initializer "web_console.mount_point" do
43
39
  if mount_point = config.web_console.mount_point
44
- Middleware.mount_point = mount_point.chomp('/')
40
+ Middleware.mount_point = mount_point.chomp("/")
41
+ end
42
+
43
+ if root = Rails.application.config.relative_url_root
44
+ Middleware.mount_point = File.join(root, Middleware.mount_point)
45
45
  end
46
46
  end
47
47
 
48
- initializer 'web_console.template_paths' do
48
+ initializer "web_console.template_paths" do
49
49
  if template_paths = config.web_console.template_paths
50
50
  Template.template_paths.unshift(*Array(template_paths))
51
51
  end
52
52
  end
53
53
 
54
- initializer 'web_console.whitelisted_ips' do
55
- if whitelisted_ips = config.web_console.whitelisted_ips
56
- Request.whitelisted_ips = Whitelist.new(whitelisted_ips)
54
+ initializer "web_console.permissions" do
55
+ permissions = web_console_permissions
56
+ Request.permissions = Permissions.new(permissions)
57
+ end
58
+
59
+ def web_console_permissions
60
+ case
61
+ when config.web_console.permissions
62
+ config.web_console.permissions
63
+ when config.web_console.allowed_ips
64
+ config.web_console.allowed_ips
65
+ when config.web_console.whitelisted_ips
66
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
67
+ The config.web_console.whitelisted_ips is deprecated and will be ignored in future release of web_console.
68
+ Please use config.web_console.allowed_ips instead.
69
+ MSG
70
+ config.web_console.whitelisted_ips
57
71
  end
58
72
  end
59
73
 
60
- initializer 'web_console.whiny_requests' do
74
+ initializer "web_console.whiny_requests" do
61
75
  if config.web_console.key?(:whiny_requests)
62
76
  Middleware.whiny_requests = config.web_console.whiny_requests
63
77
  end
64
78
  end
65
79
 
66
- initializer 'i18n.load_path' do
67
- config.i18n.load_path.concat(Dir[File.expand_path('../locales/*.yml', __FILE__)])
80
+ initializer "i18n.load_path" do
81
+ config.i18n.load_path.concat(Dir[File.expand_path("../locales/*.yml", __FILE__)])
68
82
  end
69
83
  end
70
84
  end
@@ -1,29 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module WebConsole
2
- # Web Console tailored request object.
3
4
  class Request < ActionDispatch::Request
4
- # Configurable set of whitelisted networks.
5
- cattr_accessor :whitelisted_ips
6
- @@whitelisted_ips = Whitelist.new
7
-
8
- # Define a vendor MIME type. We can call it using Mime::WEB_CONSOLE_V2 constant.
9
- Mime::Type.register 'application/vnd.web-console.v2', :web_console_v2
5
+ cattr_accessor :permissions, default: Permissions.new
10
6
 
11
- # Returns whether a request came from a whitelisted IP.
12
- #
13
- # For a request to hit Web Console features, it needs to come from a white
14
- # listed IP.
15
- def from_whitelited_ip?
16
- whitelisted_ips.include?(strict_remote_ip)
7
+ def permitted?
8
+ permissions.include?(strict_remote_ip)
17
9
  end
18
10
 
19
- # Determines the remote IP using our much stricter whitelist.
20
11
  def strict_remote_ip
21
- GetSecureIp.new(self, whitelisted_ips).to_s
22
- end
23
-
24
- # Returns whether the request is acceptable.
25
- def acceptable?
26
- xhr? && accepts.any? { |mime| Mime::WEB_CONSOLE_V2 == mime }
12
+ GetSecureIp.new(self, permissions).to_s
13
+ rescue ActionDispatch::RemoteIp::IpSpoofAttackError
14
+ "[Spoofed]"
27
15
  end
28
16
 
29
17
  private
@@ -1,16 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module WebConsole
2
- # A session lets you persist wrap an +Evaluator+ instance in memory
3
- # associated with multiple bindings.
4
+ # A session lets you persist an +Evaluator+ instance in memory associated
5
+ # with multiple bindings.
4
6
  #
5
7
  # Each newly created session is persisted into memory and you can find it
6
- # later its +id+.
8
+ # later by its +id+.
7
9
  #
8
10
  # A session may be associated with multiple bindings. This is used by the
9
11
  # error pages only, as currently, this is the only client that needs to do
10
12
  # that.
11
13
  class Session
12
- cattr_reader :inmemory_storage
13
- @@inmemory_storage = {}
14
+ cattr_reader :inmemory_storage, default: {}
14
15
 
15
16
  class << self
16
17
  # Finds a persisted session in memory by its id.
@@ -21,24 +22,30 @@ module WebConsole
21
22
  inmemory_storage[id]
22
23
  end
23
24
 
24
- # Create a Session from an exception.
25
- def from_exception(exc)
26
- new(exc.bindings)
27
- end
28
-
29
- # Create a Session from a single binding.
30
- def from_binding(binding)
31
- new(binding)
25
+ # Create a Session from an binding or exception in a storage.
26
+ #
27
+ # The storage is expected to respond to #[]. The binding is expected in
28
+ # :__web_console_binding and the exception in :__web_console_exception.
29
+ #
30
+ # Can return nil, if no binding or exception have been preserved in the
31
+ # storage.
32
+ def from(storage)
33
+ if exc = storage[:__web_console_exception]
34
+ new(ExceptionMapper.follow(exc))
35
+ elsif binding = storage[:__web_console_binding]
36
+ new([[binding]])
37
+ end
32
38
  end
33
39
  end
34
40
 
35
41
  # An unique identifier for every REPL.
36
42
  attr_reader :id
37
43
 
38
- def initialize(bindings)
44
+ def initialize(exception_mappers)
39
45
  @id = SecureRandom.hex(16)
40
- @bindings = Array(bindings)
41
- @evaluator = Evaluator.new(@bindings[0])
46
+
47
+ @exception_mappers = exception_mappers
48
+ @evaluator = Evaluator.new(@current_binding = exception_mappers.first.first)
42
49
 
43
50
  store_into_memory
44
51
  end
@@ -53,8 +60,15 @@ module WebConsole
53
60
  # Switches the current binding to the one at specified +index+.
54
61
  #
55
62
  # Returns nothing.
56
- def switch_binding_to(index)
57
- @evaluator = Evaluator.new(@bindings[index.to_i])
63
+ def switch_binding_to(index, exception_object_id)
64
+ bindings = ExceptionMapper.find_binding(@exception_mappers, exception_object_id)
65
+
66
+ @evaluator = Evaluator.new(@current_binding = bindings[index.to_i])
67
+ end
68
+
69
+ # Returns context of the current binding
70
+ def context(objpath)
71
+ Context.new(@current_binding).extract(objpath)
58
72
  end
59
73
 
60
74
  private
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SourceLocation
4
+ def initialize(binding)
5
+ @binding = binding
6
+ end
7
+
8
+ if RUBY_VERSION >= "2.6"
9
+ def path() @binding.source_location.first end
10
+ def lineno() @binding.source_location.last end
11
+ else
12
+ def path() @binding.eval("__FILE__") end
13
+ def lineno() @binding.eval("__LINE__") end
14
+ end
15
+ end
@@ -1,23 +1,25 @@
1
+ # frozen_string_literal: true
2
+
1
3
  namespace :ext do
2
- rootdir = Pathname('extensions')
4
+ rootdir = Pathname("extensions")
3
5
 
4
- desc 'Build Chrome Extension'
5
- task chrome: 'chrome:build'
6
+ desc "Build Chrome Extension"
7
+ task chrome: "chrome:build"
6
8
 
7
9
  namespace :chrome do
8
- dist = Pathname('dist/crx')
10
+ dist = Pathname("dist/crx")
9
11
  extdir = rootdir.join(dist)
10
- manifest_json = rootdir.join('chrome/manifest.json')
12
+ manifest_json = rootdir.join("chrome/manifest.json")
11
13
 
12
14
  directory extdir
13
15
 
14
- task build: [ extdir, 'lib:templates' ] do
16
+ task build: [ extdir, "lib:templates" ] do
15
17
  cd rootdir do
16
- cp_r [ 'img/', 'tmp/lib/' ], dist
18
+ cp_r [ "img/", "tmp/lib/" ], dist
17
19
  `cd chrome && git ls-files`.split("\n").each do |src|
18
20
  dest = dist.join(src)
19
21
  mkdir_p dest.dirname
20
- cp Pathname('chrome').join(src), dest
22
+ cp Pathname("chrome").join(src), dest
21
23
  end
22
24
  end
23
25
  end
@@ -34,7 +36,7 @@ namespace :ext do
34
36
  cd(extdir) { sh "zip -r ../crx-web-console-#{version}.zip ./" }
35
37
  end
36
38
 
37
- desc 'Launch a browser with the chrome extension.'
39
+ desc "Launch a browser with the chrome extension."
38
40
  task run: [ :build ] do
39
41
  cd(rootdir) { sh "sh ./script/run_chrome.sh --load-extension=#{dist}" }
40
42
  end
@@ -45,15 +47,15 @@ namespace :ext do
45
47
  end
46
48
 
47
49
  namespace :lib do
48
- templates = Pathname('lib/web_console/templates')
49
- tmplib = rootdir.join('tmp/lib/')
50
- js_erb = FileList.new(templates.join('**/*.js.erb'))
50
+ templates = Pathname("lib/web_console/templates")
51
+ tmplib = rootdir.join("tmp/lib/")
52
+ js_erb = FileList.new(templates.join("**/*.js.erb"))
51
53
  dirs = js_erb.pathmap("%{^#{templates},#{tmplib}}d")
52
54
 
53
55
  task templates: dirs + js_erb.pathmap("%{^#{templates},#{tmplib}}X")
54
56
 
55
57
  dirs.each { |d| directory d }
56
- rule '.js' => [ "%{^#{tmplib},#{templates}}X.js.erb" ] do |t|
58
+ rule ".js" => [ "%{^#{tmplib},#{templates}}X.js.erb" ] do |t|
57
59
  File.write(t.name, WebConsole::Testing::ERBPrecompiler.new(t.source).build)
58
60
  end
59
61
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :templates do
4
+ desc "Run tests for templates"
5
+ task test: [ :daemonize, :npm, :rackup, :wait, :mocha, :kill, :exit ]
6
+ task serve: [ :npm, :rackup ]
7
+
8
+ workdir = Pathname(EXPANDED_CWD).join("test/templates")
9
+ pid = Pathname(Dir.tmpdir).join("web_console_test.pid")
10
+ runner = URI.parse("http://#{ENV['IP'] || '127.0.0.1'}:#{ENV['PORT'] || 29292}/html/test_runner.html")
11
+ rackup = "rackup --host #{runner.host} --port #{runner.port}"
12
+ result = nil
13
+ browser = "phantomjs"
14
+
15
+ def need_to_wait?(uri)
16
+ Net::HTTP.start(uri.host, uri.port) { |http| http.get(uri.path) }
17
+ rescue Errno::ECONNREFUSED
18
+ retry if yield
19
+ end
20
+
21
+ task :daemonize do
22
+ rackup += " -D --pid #{pid}"
23
+ end
24
+
25
+ task npm: [ :phantomjs ] do
26
+ Dir.chdir(workdir) { system "npm install --silent" }
27
+ end
28
+
29
+ task :phantomjs do
30
+ unless system("which #{browser} >/dev/null")
31
+ browser = "./node_modules/.bin/phantomjs"
32
+ Dir.chdir(workdir) { system("test -f #{browser} || npm install --silent phantomjs-prebuilt") }
33
+ end
34
+ end
35
+
36
+ task :rackup do
37
+ Dir.chdir(workdir) { system rackup }
38
+ end
39
+
40
+ task :wait do
41
+ cnt = 0
42
+ need_to_wait?(runner) { sleep 1; cnt += 1; cnt < 5 }
43
+ end
44
+
45
+ task :mocha do
46
+ Dir.chdir(workdir) { result = system("#{browser} ./node_modules/mocha-phantomjs-core/mocha-phantomjs-core.js #{runner} dot") }
47
+ end
48
+
49
+ task :kill do
50
+ system "kill #{File.read pid}"
51
+ end
52
+
53
+ task :exit do
54
+ exit result
55
+ end
56
+ end