rails_live_reload 0.1.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 98a01974fe3a99ed6546f43f777fc3a0e91d7f175179e561494740e95d3de9fe
4
- data.tar.gz: b22be442a32123b9e09b2649993b9dc865276fd4157ff4f34cdfbc4891fb7745
3
+ metadata.gz: 88846a7af50ff0f2941eeb26795f7156ef4c92b8397453d9810d0196a27851ba
4
+ data.tar.gz: 70337f1ce69e0fab74a7272633629a565f6106a86e00590e4eb0f2d49e71281e
5
5
  SHA512:
6
- metadata.gz: a9b46ace63dbac0a9854e44676556799ad58e3a97f0c188e99dfec77a07cc36f550c7767b281946e9331a71aa870c8494c81b45ae9a7ddbed663fd5ab9a447d8
7
- data.tar.gz: 1bb0e9c9bfe6c4d59e70ced62c591907f01ec058ef2733247cdc4be299d652898200e07aeff9cf9112a5df1cf18cc71579df637c9f7dc66a48631ed18458b71f
6
+ metadata.gz: 5c4a756e0c3cd05ae4e8d75c5d221d2bd18ec17bd36d70ad8b720244f849a058d3788ec401d5f53eb88e99d0cfe8c5f09afd8610acaaf60e194b162465eb6bcf
7
+ data.tar.gz: e23b0b4b83ed08589cd9a11066419e91fb58f4d3cf2dff4724de276408877b328829b96c598edaf77f9c90b251d8993486472bdde4a7d1bd88f5ed7c27c5e68c
data/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  This is the simplest and probably the most robust way to add live reloading to your Rails app.
8
8
 
9
- Just add the gem and thats it, now you have a live reloading. No 3rd party dependencies.
9
+ Just add the gem and thats it, now you have a live reloading. **You don't need anything other than this gem for live reloading to work**.
10
10
 
11
11
  Works with:
12
12
 
@@ -40,25 +40,12 @@ $ bundle
40
40
 
41
41
  ## Configuration
42
42
 
43
- ### There are two modes:
44
- 1. `:long_polling` - This is a default mode, it uses [long polling](https://javascript.info/long-polling) techunique, client opens a connection that will hang until either change is detected, or timeout happens, if later, a new connection is oppened
45
- 2. `:polling` - This mode will use regular polling to detect changes, you can configure custom `polling_interval` (default is 100ms). We recommend using `:long_polling` as it makes much less requests to the server.
46
-
47
43
  ### Create initializer `config/initializers/rails_live_reload.rb`:
48
44
 
49
45
 
50
46
  ```ruby
51
47
  RailsLiveReload.configure do |config|
52
- # config.url = "/rails/live/reload"
53
- # Available modes are: :long_polling (default) and :polling
54
- # config.mode = :long_polling
55
-
56
- # This is used with :long_polling mode
57
- # config.timeout = 30
58
- # config.long_polling_sleep_duration = 0.1
59
-
60
- # This is used with :polling mode
61
- # config.polling_interval = 100
48
+ # config.url = "/rails/live/reload"
62
49
 
63
50
  # Default watched folders & files
64
51
  # config.watch %r{app/views/.+\.(erb|haml|slim)$}
@@ -67,6 +54,8 @@ RailsLiveReload.configure do |config|
67
54
  # More examples:
68
55
  # config.watch %r{app/helpers/.+\.rb}, reload: :always
69
56
  # config.watch %r{config/locales/.+\.yml}, reload: :always
57
+
58
+ # config.enabled = Rails.env.development?
70
59
  end if defined?(RailsLiveReload)
71
60
  ```
72
61
 
@@ -76,7 +65,7 @@ There are 3 main parts:
76
65
 
77
66
  1) listener of file changes (using `listen` gem)
78
67
  2) collector of rendered views (see rails instrumentation)
79
- 3) middleware which is responding to polling JS calls
68
+ 3) JavaScript client that communicates with server and triggers reloading when needed
80
69
 
81
70
  ## Notes
82
71
 
@@ -89,15 +78,13 @@ You are welcome to contribute. See list of `TODO's` below.
89
78
  ## TODO
90
79
 
91
80
  - reload CSS without reloading the whole page?
92
- - add `:websocket` mode?
93
81
  - smarter reload if there is a change in helper (check methods from rendered views?)
94
82
  - generator for initializer
95
83
  - more complex rules? e.g. if "user.rb" file is changed - reload all pages with rendered "users" views
96
84
  - check with older Rails versions
97
85
  - tests or specs
98
86
  - CI (github actions)
99
- - improve how JS code is injected into HTML
100
- - improve work with turbo, turbolinks
87
+ - auto reload when rendered controller was changed
101
88
 
102
89
  ## License
103
90
 
@@ -1,11 +1,18 @@
1
1
  module RailsLiveReload
2
2
  class Checker
3
+ def self.files
4
+ @files
5
+ end
6
+
7
+ def self.files=(files)
8
+ @files = files
9
+ end
3
10
 
4
11
  def self.scan(dt, rendered_files)
5
12
  temp = []
6
13
 
7
14
  # all changed files
8
- RailsLiveReload.watcher.files.each do |file, fdt|
15
+ files.each do |file, fdt|
9
16
  temp << file if fdt && fdt > dt
10
17
  end
11
18
 
@@ -25,6 +32,5 @@ module RailsLiveReload
25
32
 
26
33
  result
27
34
  end
28
-
29
35
  end
30
- end
36
+ end
@@ -19,17 +19,13 @@ module RailsLiveReload
19
19
 
20
20
  class Config
21
21
  attr_reader :patterns
22
- attr_accessor :mode, :polling_interval, :timeout, :url, :watcher, :files, :enabled, :long_polling_sleep_duration
22
+ attr_accessor :url, :watcher, :files, :enabled
23
23
 
24
24
  def initialize
25
- @mode = :long_polling
26
- @timeout = 30
27
- @long_polling_sleep_duration = 0.1
28
- @polling_interval = 100
29
25
  @url = "/rails/live/reload"
30
26
  @watcher = nil
31
27
  @files = {}
32
- @enabled = ::Rails.env.development? && !defined?(Rails::Console)
28
+ @enabled = ::Rails.env.development?
33
29
 
34
30
  # These configs work for 95% apps, see README for more info
35
31
  @patterns = {
@@ -39,6 +35,10 @@ module RailsLiveReload
39
35
  @default_patterns_changed = false
40
36
  end
41
37
 
38
+ def root_path
39
+ @root_path ||= ::Rails.application.root
40
+ end
41
+
42
42
  def watch(pattern, reload: :on_change)
43
43
  unless @default_patterns_changed
44
44
  @default_patterns_changed = true
@@ -47,5 +47,9 @@ module RailsLiveReload
47
47
 
48
48
  patterns[pattern] = reload
49
49
  end
50
+
51
+ def socket_path
52
+ root_path.join('tmp/sockets/rails_live_reload.sock')
53
+ end
50
54
  end
51
55
  end
@@ -1,32 +1,41 @@
1
1
  module RailsLiveReload
2
2
  class Railtie < ::Rails::Engine
3
-
4
- if RailsLiveReload.enabled?
3
+ if RailsLiveReload.enabled? && defined?(::Rails::Server)
5
4
  initializer "rails_live_reload.middleware" do |app|
6
5
  if ::Rails::VERSION::MAJOR.to_i >= 5
7
- app.middleware.insert_after ActionDispatch::Executor, RailsLiveReload.middleware
6
+ app.middleware.insert_after ActionDispatch::Executor, RailsLiveReload::Middleware::Base
8
7
  else
9
8
  begin
10
- app.middleware.insert_after ActionDispatch::Static, RailsLiveReload.middleware
9
+ app.middleware.insert_after ActionDispatch::Static, RailsLiveReload::Middleware::Base
11
10
  rescue
12
- app.middleware.insert_after Rack::SendFile, RailsLiveReload.middleware
11
+ app.middleware.insert_after Rack::SendFile, RailsLiveReload::Middleware::Base
13
12
  end
14
13
  end
14
+ end
15
15
 
16
+ initializer "rails_live_reload.watcher" do
16
17
  RailsLiveReload::Watcher.init
17
18
  end
18
19
 
19
- initializer :configure_metrics, after: :initialize_logger do
20
+ initializer "rails_live_reload.configure_metrics", after: :initialize_logger do
20
21
  ActiveSupport::Notifications.subscribe(
21
22
  /\.action_view/,
22
23
  RailsLiveReload::Instrument::MetricsCollector.new
23
24
  )
24
25
  end
25
26
 
26
- initializer :reset_current_request, after: :initialize_logger do |app|
27
+ initializer "rails_live_reload.reset_current_request", after: :initialize_logger do |app|
27
28
  app.executor.to_run { CurrentRequest.cleanup }
28
29
  app.executor.to_complete { CurrentRequest.cleanup }
29
30
  end
31
+
32
+ initializer "rails_live_reload.routes" do
33
+ config.after_initialize do |app|
34
+ app.routes.prepend do
35
+ mount RailsLiveReload.server => RailsLiveReload.config.url, internal: true, anchor: true
36
+ end
37
+ end
38
+ end
30
39
  end
31
40
  end
32
41
  end
@@ -0,0 +1,6 @@
1
+ /*!
2
+ Rails Live Reload 0.3.0
3
+ Copyright © 2022 RailsJazz
4
+ https://railsjazz.com
5
+ */
6
+ var RailsLiveReload=function(){"use strict";const t="RELOAD",e=["rails-live-reload-v1-json"];class n{static _instance;static get instance(){return n._instance||(n._instance=new this),n._instance}static start(){this.instance.start()}constructor(){this.initialize(),document.addEventListener("turbo:render",(()=>{document.documentElement.hasAttribute("data-turbo-preview")||this.restart()})),document.addEventListener("turbolinks:render",(()=>{document.documentElement.hasAttribute("data-turbolinks-preview")||this.restart()}))}initialize(){const{files:t,time:e,url:n}=JSON.parse(this.optionsNode.textContent);this.files=t,this.time=e,this.url=n}fullReload(){window.location.reload()}get optionsNode(){const t=document.getElementById("rails-live-reload-options");if(!t)throw"Unable to find RailsLiveReload options";return t}start(){this.connection||(this.connection=new WebSocket(function(t){if("function"==typeof t&&(t=t()),t&&!/^wss?:/i.test(t)){const e=document.createElement("a");return e.href=t,e.href=e.href,e.protocol=e.protocol.replace("http","ws"),e.href}return t}(this.url),e),this.connection.onmessage=this.handleMessage.bind(this),this.connection.onopen=this.handleConnectionOpen.bind(this),this.connection.onclose=this.handleConnectionClosed.bind(this))}stop(){this.connection.close()}restart(){this.initialize(),this.setupConnection()}setupConnection(){this.connection.send(JSON.stringify({event:"setup",options:{files:this.files,dt:this.time}}))}handleConnectionOpen(t){this.retriesCount=0,this.setupConnection()}handleMessage(e){JSON.parse(e.data).command===t&&this.fullReload()}handleConnectionClosed(t){this.connection=void 0,!t.wasClean&&this.retriesCount<=10&&(this.retriesCount++,setTimeout((()=>{this.start()}),1e3*this.retriesCount))}}return document.addEventListener("DOMContentLoaded",(()=>{n.start()})),n}();
@@ -0,0 +1,55 @@
1
+ module RailsLiveReload
2
+ module Middleware
3
+ class Base
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ dup.call!(env)
10
+ end
11
+
12
+ def call!(env)
13
+ if env["REQUEST_PATH"].starts_with?(RailsLiveReload.config.url)
14
+ ::Rails.logger.silence do
15
+ @app.call(env)
16
+ end
17
+ else
18
+ request = Rack::Request.new(env)
19
+ status, headers, response = @app.call(env)
20
+
21
+ if html?(headers) && response.respond_to?(:[]) && (status == 500 || (status.to_s =~ /20./ && request.get?))
22
+ new_response = make_new_response(response[0])
23
+ headers['Content-Length'] = new_response.bytesize.to_s
24
+ response = [new_response]
25
+ end
26
+
27
+ [status, headers, response]
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def make_new_response(body)
34
+ body = body.sub("</head>", <<~HTML.html_safe)
35
+ <script defer type="text/javascript" src="#{RailsLiveReload.config.url}/script"></script>
36
+ </head>
37
+ HTML
38
+ body.sub("</body>", <<~HTML.html_safe)
39
+ <script id="rails-live-reload-options" type="application/json">
40
+ #{{
41
+ files: CurrentRequest.current.data.to_a,
42
+ time: Time.now.to_i,
43
+ url: RailsLiveReload.config.url
44
+ }.to_json}
45
+ </script>
46
+ </body>
47
+ HTML
48
+ end
49
+
50
+ def html?(headers)
51
+ headers["Content-Type"].to_s.include?("text/html")
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,72 @@
1
+ require 'rails_live_reload/web_socket/event_loop'
2
+ require 'rails_live_reload/web_socket/message_buffer'
3
+ require 'rails_live_reload/web_socket/wrapper'
4
+ require 'rails_live_reload/web_socket/client_socket'
5
+ require 'rails_live_reload/web_socket/stream'
6
+ require 'rails_live_reload/web_socket/base'
7
+
8
+ module RailsLiveReload
9
+ module Server
10
+ # This class is based on ActionCable
11
+ # https://github.com/rails/rails/blob/v7.0.3/actioncable/lib/action_cable/server/base.rb
12
+ class Base
13
+ include RailsLiveReload::Server::Connections
14
+
15
+ attr_reader :mutex
16
+
17
+ def reload_all
18
+ @mutex.synchronize do
19
+ connections.each(&:reload)
20
+ end
21
+ end
22
+
23
+ def initialize
24
+ @mutex = Monitor.new
25
+ @event_loop = nil
26
+ end
27
+
28
+ # Called by Rack to set up the server.
29
+ def call(env)
30
+ case env["REQUEST_PATH"]
31
+ when RailsLiveReload.config.url
32
+ setup_socket
33
+ setup_heartbeat_timer
34
+ request = Rack::Request.new(env)
35
+ RailsLiveReload::WebSocket::Base.new(self, request).process
36
+ when "#{RailsLiveReload.config.url}/script"
37
+ content = client_javascript
38
+ [200, {'Content-Type' => 'application/javascript', 'Content-Length' => content.size.to_s, 'Cache-Control' => 'no-store'}, [content]]
39
+ else
40
+ raise ActionController::RoutingError, 'Not found'
41
+ end
42
+ end
43
+
44
+ def client_javascript
45
+ @client_javascript || @mutex.synchronize { @client_javascript ||= File.open(File.join(File.dirname(__FILE__), "../javascript/websocket.js")).read }
46
+ end
47
+
48
+ def event_loop
49
+ @event_loop || @mutex.synchronize { @event_loop ||= RailsLiveReload::WebSocket::EventLoop.new }
50
+ end
51
+
52
+ def setup_socket
53
+ @socket ||= UNIXSocket.open(RailsLiveReload.config.socket_path).tap do |socket|
54
+ Thread.new do
55
+ loop do
56
+ data = JSON.parse socket.readline
57
+
58
+ case data["event"]
59
+ when RailsLiveReload::INTERNAL[:socket_events][:reload]
60
+ RailsLiveReload::Checker.files = data['files']
61
+
62
+ reload_all
63
+ else
64
+ raise NotImplementedError
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,27 @@
1
+ module RailsLiveReload
2
+ module Server
3
+ # This class is strongly based on ActionCable
4
+ # https://github.com/rails/rails/blob/v7.0.3/actioncable/lib/action_cable/server/connections.rb
5
+ module Connections
6
+ BEAT_INTERVAL = 3
7
+
8
+ def connections
9
+ @connections || @mutex.synchronize { @connections ||= Concurrent::Array.new }
10
+ end
11
+
12
+ def add_connection(connection)
13
+ connections << connection
14
+ end
15
+
16
+ def remove_connection(connection)
17
+ connections.delete connection
18
+ end
19
+
20
+ def setup_heartbeat_timer
21
+ @heartbeat_timer ||= event_loop.timer(BEAT_INTERVAL) do
22
+ event_loop.post { connections.each(&:beat) }
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,3 +1,3 @@
1
1
  module RailsLiveReload
2
- VERSION = "0.1.1"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -1,6 +1,10 @@
1
1
  module RailsLiveReload
2
2
  class Watcher
3
- attr_reader :root, :files
3
+ attr_reader :files, :sockets
4
+
5
+ def root
6
+ RailsLiveReload.config.root_path
7
+ end
4
8
 
5
9
  def Watcher.init
6
10
  watcher = new
@@ -8,15 +12,16 @@ module RailsLiveReload
8
12
  end
9
13
 
10
14
  def initialize
11
- @root = ::Rails.application.root
12
15
  @files = {}
16
+ @sockets = []
13
17
 
14
18
  puts "Watching: #{root}"
15
19
  RailsLiveReload.patterns.each do |pattern, rule|
16
- puts " #{pattern} => #{rule}"
20
+ puts " #{pattern} => #{rule}"
17
21
  end
18
22
 
19
23
  build_tree
24
+ start_socket
20
25
  start_listener
21
26
  end
22
27
 
@@ -27,6 +32,7 @@ module RailsLiveReload
27
32
  all.each do |file|
28
33
  files[file] = File.mtime(file).to_i rescue nil
29
34
  end
35
+ reload_all
30
36
  end
31
37
  listener.start
32
38
  end
@@ -37,5 +43,35 @@ module RailsLiveReload
37
43
  files[file] = File.mtime(file).to_i rescue nil
38
44
  end
39
45
  end
46
+
47
+ def reload_all
48
+ data = {
49
+ event: RailsLiveReload::INTERNAL[:socket_events][:reload],
50
+ files: files
51
+ }.to_json
52
+
53
+ @sockets.each do |socket, _|
54
+ socket.puts data
55
+ end
56
+ end
57
+
58
+ def start_socket
59
+ Thread.new do
60
+ Socket.unix_server_socket(RailsLiveReload.config.socket_path.to_s) do |sock|
61
+ loop do
62
+ socket, _ = sock.accept
63
+ sockets << socket
64
+ Thread.new do
65
+ begin
66
+ socket.eof
67
+ ensure
68
+ socket.close
69
+ sockets.delete socket
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
40
76
  end
41
77
  end
@@ -0,0 +1,116 @@
1
+ module RailsLiveReload
2
+ module WebSocket
3
+ # This class is strongly based on ActionCable
4
+ # https://github.com/rails/rails/blob/v7.0.3/actioncable/lib/action_cable/connection/base.rb
5
+ class Base
6
+ attr_reader :server, :env, :protocol, :request
7
+ attr_reader :dt, :files
8
+
9
+ delegate :event_loop, to: :server
10
+
11
+ def initialize(server, request)
12
+ @server, @request = server, request
13
+ @env = request.env
14
+ @websocket = RailsLiveReload::WebSocket::Wrapper.new(env, self, event_loop)
15
+ @message_buffer = RailsLiveReload::WebSocket::MessageBuffer.new(self)
16
+ end
17
+
18
+ def process
19
+ if websocket.possible?
20
+ respond_to_successful_request
21
+ else
22
+ respond_to_invalid_request
23
+ end
24
+ end
25
+
26
+ def receive(websocket_message)
27
+ if websocket.alive?
28
+ handle_channel_command decode(websocket_message)
29
+ end
30
+ end
31
+
32
+ def handle_channel_command(payload)
33
+ case payload['event']
34
+ when 'setup'
35
+ setup payload['options']
36
+ else
37
+ raise NotImplementedError
38
+ end
39
+ end
40
+
41
+ def reload
42
+ return if dt.nil? || files.nil? || RailsLiveReload::Checker.scan(dt, files).size.zero?
43
+
44
+ transmit({command: "RELOAD"})
45
+ end
46
+
47
+ def transmit(cable_message)
48
+ websocket.transmit encode(cable_message)
49
+ end
50
+
51
+ def close(reason: nil)
52
+ transmit({
53
+ type: RailsLiveReload::INTERNAL[:message_types][:disconnect],
54
+ reason: reason
55
+ })
56
+ websocket.close
57
+ end
58
+
59
+ def beat
60
+ transmit type: RailsLiveReload::INTERNAL[:message_types][:ping], message: Time.now.to_i
61
+ end
62
+
63
+ def on_open
64
+ @protocol = websocket.protocol
65
+ send_welcome_message
66
+
67
+ message_buffer.process!
68
+ server.add_connection(self)
69
+ end
70
+
71
+ def on_message(message)
72
+ message_buffer.append message
73
+ end
74
+
75
+ def on_error(message)
76
+ raise message
77
+ end
78
+
79
+ def on_close(reason, code)
80
+ server.remove_connection(self)
81
+ end
82
+
83
+ private
84
+
85
+ attr_reader :websocket
86
+ attr_reader :message_buffer
87
+
88
+ def setup(options)
89
+ @dt = options['dt']
90
+ @files = options['files']
91
+ end
92
+
93
+ def encode(message)
94
+ message.to_json
95
+ end
96
+
97
+ def decode(message)
98
+ JSON.parse message
99
+ end
100
+
101
+ def send_welcome_message
102
+ transmit type: RailsLiveReload::INTERNAL[:message_types][:welcome]
103
+ end
104
+
105
+ def respond_to_successful_request
106
+ websocket.rack_response
107
+ end
108
+
109
+ def respond_to_invalid_request
110
+ close(reason: RailsLiveReload::INTERNAL[:disconnect_reasons][:invalid_request]) if websocket.alive?
111
+
112
+ [ 404, { "Content-Type" => "text/plain" }, [ "Page not found" ] ]
113
+ end
114
+ end
115
+ end
116
+ end