opal_hot_reloader 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4556d3643d7dae2b15f95ee81bc895b3f4d7d5e9
4
+ data.tar.gz: dc4f19e56f5f325f98b2d317c6d05b74acd6e1b9
5
+ SHA512:
6
+ metadata.gz: b202a24741fd488838f95030164a223c9f344e60b772cd78d40a56ceafb764e90dc89869c7e45e3a1c334db5d6f05c026b9d66676254db5aca3e86437a38fc22
7
+ data.tar.gz: 608dd3b9986e0343b3d6d1bc5b876af7d86319b72fef758c924c560d49706e098bd94c3131084d7a69fac7aff89ece56b7fbab02456dcd0bb3ab5037a45de321
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.1
4
+ before_install: gem install bundler -v 1.10.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in opal_hot_reloader.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Forrest Chang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,143 @@
1
+ # OpalHotReloader
2
+
3
+ opal-hot-reloader is a hot reloader for [Opal](http://opalrb.org). It has built in [react.rb](http://reactrb.org) support and can be extended to support an arbitrary hook to be run after code is evaluted. It watches directories specified and when a file is modified it pushes the change via websocket to the client. opal-hot-reloader reloader will reload the following without reloading the whole page and destroying any state the page has.
4
+ - opal code
5
+ - css (currently supporting Rack::Sass:Place and Rails asset pipeline)
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'opal_hot_reloader' # currently on github only, gem coming soon
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install opal_hot_reloader
22
+
23
+
24
+ ## Usage
25
+
26
+ ### Server Setup
27
+
28
+ #### NOTE: fixed for Opal 0.10 as of commit d2dc849 - repull if not working for 0.10.0
29
+
30
+ After adding `gem "opal_hot_loader"` to your gemfile, you must start the server-side part. This will allow websocket connections, and whenever a file is changed it will send it via the socket to listening clients.
31
+
32
+ To start the server-side of the hotloader:
33
+ ```
34
+ opal-hot-reloader -p 25222 -d dir1,dir2,dir3
35
+
36
+ Usage: opal-hot-reloader [options]
37
+ -p, --port [INTEGER] port to run on, defaults to 25222
38
+ -d, --directories x,y,z comma separated directories to watch
39
+ ```
40
+
41
+ For a react.rb Rails app, opal-hot-reloader automatically includes app/assets/javascripts,app/views/components if they exist
42
+
43
+ Example adding 2 directories
44
+ ```
45
+ opal-hot-reloader -d app/js,app/client/components
46
+ ```
47
+
48
+ You may consider using [foreman](https://github.com/ddollar/foreman/)
49
+ and starting the Rails server and hot reloader at the same time. If
50
+ you are doing react.rb development w/Rails, you may already be doing
51
+ so with the Rails server and webpack.
52
+
53
+
54
+ ### Client Setup
55
+
56
+ Require in an opal file (for opal-rails apps application.js.rb is a good place) and start listening for changes:
57
+
58
+ #### Note: OpalHotReloader.listen() deprecation
59
+ OpalHotReloader.listen() used to take a 2nd Boolean parameter to signify a reactrb app. This is deprecated and no longer needed.
60
+
61
+ ```ruby
62
+ require 'opal_hot_reloader'
63
+
64
+ # @param port [Integer] opal hot reloader port to connect to. Defaults to 25222 to match opal-hot-loader default
65
+ OpalHotReloader.listen(25222)
66
+ ```
67
+
68
+ If you are using the default port then you can just call:
69
+ ```
70
+ OpalHotReloader.listen
71
+ ```
72
+
73
+ This will open up a websocket from the client to the server on the given port. The server-side should already be running.
74
+
75
+ Enjoy!
76
+
77
+ ## Vision
78
+
79
+ Some of you might be asking? Why do this, isn't this reinventing the
80
+ wheel by programs like webpack, etc.? I should mention that
81
+ reinventing the wheel seems happens all the time in the Javascript
82
+ world.
83
+
84
+ Yes and no. opal-hot-reloader is an "All Ruby(Opal)", self contained
85
+ system, so if you're doing any kind of Opal frontend/Ruby backend
86
+ webserver type of project, you will be able to just drop in
87
+ opal-hot-reloader and it will work out of the box without having
88
+ install/configure webpack or similar.
89
+
90
+ I believe it will be most advantageous for Opal to be able to straddle
91
+ a hybrid approach where:
92
+
93
+ * With Opal and Rails, we use the existing mechanism, sprockets, to
94
+ serve up all the things it does in the "normal" Rails ecosystem. I.e
95
+ we want to work with the system. We want all the perks of Ruby and
96
+ Rails without have to hand cobble it ourselves
97
+ * We use webpack or similar for being a "1st class JS citizen". This
98
+ gives us access to all the frontend assets in npm, we want all those
99
+ options and perks.
100
+
101
+ While I do favor moving as much Javascript to webpack, following suit
102
+ to React.js's lead, I see an "all webpack solution" for Opal apps
103
+ being only one of a few permutations, and not particularly appealing
104
+ to most Rails programmers - who I think is the largest demographic
105
+ likely to want to do Opal programming.
106
+
107
+ While we wait for the other approaches to evolve and get implemented
108
+ this solution is here and works now. It works with an "All Ruby"
109
+ system, it works with a Rails app that is using webpack to provide
110
+ react.js components to react.rb.
111
+
112
+ ### Goals
113
+ * Bring the benefits of "leading edge web development" to All Ruby
114
+ (via Opal) full stack development. One of my efforts to be more
115
+ like Einstein in “Opening up yet another fragment of the frontier of
116
+ beauty” - i.e. share the joy.
117
+ * Batteries included out of the box - make it (increasingingly) easy
118
+ for Rubyists to enjoy the previous goal. This is a manifestation of
119
+ the "Ruby Way" of making the programmer happy
120
+ * Try to add the least amount of additional dependencies to projects
121
+ it's used in
122
+
123
+
124
+
125
+ ## Screencasts
126
+
127
+ * Quickie intro to opal-hot-reloader https://youtu.be/NQbzL7fNOks
128
+
129
+ ## Development
130
+
131
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
132
+
133
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
134
+
135
+ ## Contributing
136
+
137
+ Bug reports and pull requests are welcome on GitHub at https://github.com/fkchang/opal_hot_reloader.
138
+
139
+
140
+ ## License
141
+
142
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
143
+
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ require "bundler/gem_tasks"
2
+ require 'opal'
3
+ require 'opal-rspec'
4
+ require 'opal_hot_reloader' # load this server side to setup opal paths
5
+ require 'opal/sprockets/environment'
6
+ require 'opal/rspec/rake_task'
7
+
8
+ require "rspec/core/rake_task"
9
+
10
+
11
+ Opal.append_path File.expand_path('../spec-opal', __FILE__)
12
+ Opal::RSpec::RakeTask.new("opal:spec") do |server, task|
13
+ task.files = FileList['spec-opal/**/*_spec.rb']
14
+ end
15
+
16
+ RSpec::Core::RakeTask.new(:spec)
17
+
18
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "opal_hot_reloader"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require "opal_hot_reloader/server"
5
+
6
+ options = {:port => 25222, :directories => ['app']}
7
+ OptionParser.new do |opts|
8
+ opts.banner = "Usage: opal-hot-reloader [options]"
9
+
10
+ opts.on("-p", '--port [INTEGER]', Integer, 'port to run on, defaults to 25222') do |v|
11
+ options[:port] = v
12
+ end
13
+
14
+ opts.on("-d", '--directories x,y,z', Array, "comma separated directories to watch. Ex. to add 2 directories '-d app/assets/js,app/client/components'. Directoriess automatically included if they exist are:\n\t\t* app/assets/javascripts\n\t\t* app/views/components") do |v|
15
+ options[:directories] = v
16
+ end
17
+
18
+ end.parse!
19
+
20
+ server = OpalHotReloader::Server.new(options)
21
+ puts "Listening on port #{options[:port]}, watching for changes in #{options[:directories].join(', ')}"
22
+ server.loop
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,9 @@
1
+ require 'opal'
2
+ require "opal_hot_reloader/version"
3
+ require "opal_hot_reloader/server"
4
+
5
+ module OpalHotReloader
6
+ # Your code goes here...
7
+ end
8
+
9
+ Opal.append_path(File.expand_path(File.join('..', '..', 'opal'), __FILE__).untaint)
@@ -0,0 +1,284 @@
1
+ require 'websocket'
2
+ require 'socket'
3
+ require 'fiber'
4
+ require 'listen'
5
+ require 'optparse'
6
+ require 'json'
7
+
8
+ module OpalHotReloader
9
+ # Most of this lifted from https://github.com/saward/Rubame
10
+ class Server
11
+
12
+ attr_reader :directories
13
+ def initialize(options)
14
+ Socket.do_not_reverse_lookup
15
+ @hostname = '0.0.0.0'
16
+ @port = options[:port]
17
+ setup_directories(options)
18
+
19
+ @reading = []
20
+ @writing = []
21
+
22
+ @clients = {} # Socket as key, and Client as value
23
+
24
+ @socket = TCPServer.new(@hostname, @port)
25
+ @reading.push @socket
26
+ end
27
+
28
+ # adds known directories automatically if they exist
29
+ # - rails js app/assets/javascripts
30
+ # - reactrb rails defaults app/views/components
31
+ # - you tell me and I'll add them
32
+ def setup_directories(options)
33
+ @directories = options[:directories] || []
34
+ [
35
+ 'app/assets/javascripts',
36
+ 'app/views/components'
37
+ ].each { |known_dir|
38
+ if !@directories.include?(known_dir) && File.exists?(known_dir)
39
+ @directories << known_dir
40
+ end
41
+ }
42
+ end
43
+
44
+ def accept
45
+ socket = @socket.accept_nonblock
46
+ @reading.push socket
47
+ handshake = WebSocket::Handshake::Server.new
48
+ client = Client.new(socket, handshake, self)
49
+
50
+ while line = socket.gets
51
+ client.handshake << line
52
+ break if client.handshake.finished?
53
+ end
54
+ if client.handshake.valid?
55
+ @clients[socket] = client
56
+ client.write handshake.to_s
57
+ client.opened = true
58
+ return client
59
+ else
60
+ close(client)
61
+ end
62
+ return nil
63
+ end
64
+
65
+
66
+ def send_updated_file(modified_file)
67
+ if modified_file =~ /\.rb$/
68
+ file_contents = File.read(modified_file)
69
+ update = {
70
+ type: 'ruby',
71
+ filename: modified_file,
72
+ source_code: file_contents
73
+ }.to_json
74
+ end
75
+ if modified_file =~ /\.s?[ac]ss$/
76
+ # TODO: Switch from hard-wired path assumptions to using SASS/sprockets config
77
+ relative_path = Pathname.new(modified_file).relative_path_from(Pathname.new(Dir.pwd))
78
+ url = relative_path.to_s
79
+ .sub('public/','')
80
+ .sub('/sass/','/')
81
+ .sub(/\.s[ac]ss/, '.css')
82
+ update = {
83
+ type: 'css',
84
+ filename: modified_file,
85
+ url: url
86
+ }.to_json
87
+ end
88
+ if update
89
+ @clients.each { |socket, client| client.send(update) }
90
+ end
91
+ end
92
+
93
+ PROGRAM = 'opal-hot-reloader'
94
+ def loop
95
+ listener = Listen.to(*@directories, only: %r{\.(rb|s?[ac]ss)$}) do |modified, added, removed|
96
+ modified.each { |modified_file| send_updated_file(modified_file) }
97
+ puts "modified absolute path: #{modified}"
98
+ puts "added absolute path: #{added}"
99
+ puts "removed absolute path: #{removed}"
100
+ end
101
+ listener.start
102
+
103
+ puts "#{PROGRAM}: starting..."
104
+ while (!$quit)
105
+ run do |client|
106
+ client.onopen do
107
+ puts "#{PROGRAM}: client open"
108
+ end
109
+ client.onmessage do |mess|
110
+ puts "PROGRAM: message received: #{mess}"
111
+ end
112
+ client.onclose do
113
+ puts "#{PROGRAM}: client closed"
114
+ end
115
+ end
116
+ sleep 0.2
117
+ end
118
+ end
119
+
120
+ def read(client)
121
+
122
+ pairs = client.socket.recvfrom(2000)
123
+ messages = []
124
+
125
+ if pairs[0].length == 0
126
+ close(client)
127
+ else
128
+ client.frame << pairs[0]
129
+
130
+ while f = client.frame.next
131
+ if (f.type == :close)
132
+ close(client)
133
+ return messages
134
+ else
135
+ messages.push f
136
+ end
137
+ end
138
+
139
+ end
140
+
141
+ return messages
142
+
143
+ end
144
+
145
+ def close(client)
146
+ @reading.delete client.socket
147
+ @clients.delete client.socket
148
+ begin
149
+ client.socket.close
150
+ rescue
151
+ end
152
+ client.closed = true
153
+ end
154
+
155
+ def run(time = 0, &blk)
156
+ readable, writable = IO.select(@reading, @writing, nil, 0)
157
+
158
+ if readable
159
+ readable.each do |socket|
160
+ client = @clients[socket]
161
+ if socket == @socket
162
+ client = accept
163
+ else
164
+ msg = read(client)
165
+ client.messaged = msg
166
+ end
167
+
168
+ blk.call(client) if client and blk
169
+ end
170
+ end
171
+
172
+ # Check for lazy send items
173
+ timer_start = Time.now
174
+ time_passed = 0
175
+ begin
176
+ @clients.each do |s, c|
177
+ c.send_some_lazy(5)
178
+ end
179
+ time_passed = Time.now - timer_start
180
+ end while time_passed < time
181
+ end
182
+
183
+ def stop
184
+ @socket.close
185
+ end
186
+ end
187
+
188
+ class Client
189
+ attr_accessor :socket, :handshake, :frame, :opened, :messaged, :closed
190
+
191
+ def initialize(socket, handshake, server)
192
+ @socket = socket
193
+ @handshake = handshake
194
+ @frame = WebSocket::Frame::Incoming::Server.new(:version => @handshake.version)
195
+ @opened = false
196
+ @messaged = []
197
+ @lazy_queue = []
198
+ @lazy_current_queue = nil
199
+ @closed = false
200
+ @server = server
201
+ end
202
+
203
+ def write(data)
204
+ @socket.write data
205
+ end
206
+
207
+ def send(data)
208
+ frame = WebSocket::Frame::Outgoing::Server.new(:version => @handshake.version, :data => data, :type => :text)
209
+ begin
210
+ @socket.write frame
211
+ @socket.flush
212
+ rescue
213
+ @server.close(self) unless @closed
214
+ end
215
+ end
216
+
217
+ def lazy_send(data)
218
+ @lazy_queue.push data
219
+ end
220
+
221
+ def get_lazy_fiber
222
+ # Create the fiber if needed
223
+ if @lazy_fiber == nil or !@lazy_fiber.alive?
224
+ @lazy_fiber = Fiber.new do
225
+ @lazy_current_queue.each do |data|
226
+ send(data)
227
+ Fiber.yield unless @lazy_current_queue[-1] == data
228
+ end
229
+ end
230
+ end
231
+
232
+ return @lazy_fiber
233
+ end
234
+
235
+ def send_some_lazy(count)
236
+ # To save on cpu cycles, we don't want to be chopping and changing arrays, which could get quite large. Instead,
237
+ # we iterate over an array which we are sure won't change out from underneath us.
238
+ unless @lazy_current_queue
239
+ @lazy_current_queue = @lazy_queue
240
+ @lazy_queue = []
241
+ end
242
+
243
+ completed = 0
244
+ begin
245
+ get_lazy_fiber.resume
246
+ completed += 1
247
+ end while (@lazy_queue.count > 0 or @lazy_current_queue.count > 0) and completed < count
248
+
249
+ end
250
+
251
+ def onopen(&blk)
252
+ if @opened
253
+ begin
254
+ blk.call
255
+ ensure
256
+ @opened = false
257
+ end
258
+ end
259
+ end
260
+
261
+ def onmessage(&blk)
262
+ if @messaged.size > 0
263
+ begin
264
+ @messaged.each do |x|
265
+ blk.call(x.to_s)
266
+ end
267
+ ensure
268
+ @messaged = []
269
+ end
270
+ end
271
+ end
272
+
273
+ def onclose(&blk)
274
+ if @closed
275
+ begin
276
+ blk.call
277
+ ensure
278
+ end
279
+ end
280
+ end
281
+ end
282
+
283
+ line = 0
284
+ end
@@ -0,0 +1,3 @@
1
+ module OpalHotReloader
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,74 @@
1
+ require 'opal_hot_reloader/reactrb_patches'
2
+ require 'opal_hot_reloader/css_reloader'
3
+ require 'opal-parser' # gives me 'eval', for hot-loading code
4
+
5
+ require 'json'
6
+
7
+ # Opal client to support hot reloading
8
+ $eval_proc = proc { |s| eval s }
9
+ class OpalHotReloader
10
+
11
+ def connect_to_websocket(port)
12
+ host = `window.location.host`.sub(/:\d+/, '')
13
+ host = '127.0.0.1' if host == ''
14
+ ws_url = "#{host}:#{port}"
15
+ puts "Hot-Reloader connecting to #{ws_url}"
16
+ %x{
17
+ ws = new WebSocket('ws://' + #{ws_url});
18
+ // console.log(ws);
19
+ ws.onmessage = #{lambda { |e| reload(e) }}
20
+ }
21
+ end
22
+
23
+ def reload(e)
24
+ reload_request = JSON.parse(`e.data`)
25
+ if reload_request[:type] == "ruby"
26
+ puts "Reloading ruby #{reload_request[:filename]}"
27
+ $eval_proc.call reload_request[:source_code]
28
+ if @reload_post_callback
29
+ @reload_post_callback.call
30
+ else
31
+ puts "not reloading code"
32
+ end
33
+ end
34
+ if reload_request[:type] == "css"
35
+ @css_reloader.reload(reload_request, `document`)
36
+ end
37
+ end
38
+
39
+ # @param port [Integer] opal hot reloader port to connect to
40
+ # @param reload_post_callback [Proc] optional callback to be called after re evaluating a file for example in react.rb files we want to do a React::Component.force_update!
41
+ def initialize(port=25222, &reload_post_callback)
42
+ @port = port
43
+ @reload_post_callback = reload_post_callback
44
+ @css_reloader = CssReloader.new
45
+ end
46
+ # Opens a websocket connection that evaluates new files and runs the optional @reload_post_callback
47
+ def listen
48
+ connect_to_websocket(@port)
49
+ end
50
+
51
+ # convenience method to start a listen w/one line
52
+ # @param port [Integer] opal hot reloader port to connect to. Defaults to 25222 to match opal-hot-loader default
53
+ # @deprecated reactrb - this flag no longer necessary and will be removed in gem release 0.2
54
+ def self.listen(port=25222, reactrb=false)
55
+ return if @server
56
+ if reactrb
57
+ warn "OpalHotReloader.listen(#{port}): reactrb flag is deprectated and will be removed in gem release 0.2. React will automatically be detected"
58
+ end
59
+ create_framework_aware_server(port)
60
+ end
61
+ # Automatically add in framework specific hooks
62
+
63
+ def self.create_framework_aware_server(port)
64
+ if defined? ::React
65
+ ReactrbPatches.patch!
66
+ @server = OpalHotReloader.new(port) { React::Component.force_update! }
67
+ else
68
+ puts "No framework detected"
69
+ @server = OpalHotReloader.new(port)
70
+ end
71
+ @server.listen
72
+ end
73
+
74
+ end
@@ -0,0 +1,39 @@
1
+ require 'native'
2
+ class OpalHotReloader
3
+ class CssReloader
4
+
5
+ def reload(reload_request, document)
6
+ url = reload_request[:url]
7
+ puts "Reloading CSS: #{url}"
8
+ to_append = "t_hot_reload=#{Time.now.to_i}"
9
+ links = Native(`document.getElementsByTagName("link")`)
10
+ (0..links.length-1).each { |i|
11
+ link = links[i]
12
+ if link.rel == 'stylesheet' && is_matching_stylesheet?(link.href, url)
13
+ if link.href !~ /\?/
14
+ link.href += "?#{to_append}"
15
+ else
16
+ if link.href !~ /t_hot_reload/
17
+ link.href += "&#{to_append}"
18
+ else
19
+ link.href = link.href.sub(/t_hot_reload=\d+/, to_append)
20
+ end
21
+ end
22
+ end
23
+ }
24
+ end
25
+
26
+ def is_matching_stylesheet?(href, url)
27
+ # straight match, like in Rack::Sass::Plugin
28
+ if href.index(url)
29
+ true
30
+ else
31
+ # Rails asset pipeline match
32
+ url_base = File.basename(url).sub(/\.s?css+/, '').sub(/\.s?css+/, '')
33
+ href_base = File.basename(href).sub(/\.self-.*.css.+/, '')
34
+ url_base == href_base
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,6 @@
1
+ # pee
2
+ class Foo
3
+ def :bar
4
+ :bar
5
+ end
6
+ end
@@ -0,0 +1,60 @@
1
+ # patches to support reloading react.rb
2
+ class ReactrbPatches
3
+ # React.rb needs to be patched so the we don't keep adding callbacks
4
+ def self.patch!
5
+ module ::React
6
+ module Callbacks
7
+ module ClassMethods
8
+ def define_callback(callback_name)
9
+ attribute_name = "_#{callback_name}_callbacks"
10
+ class_attribute(attribute_name)
11
+ self.send("#{attribute_name}=", [])
12
+ define_singleton_method(callback_name) do |*args, &block|
13
+ # puts "calling new and improved callbacks"
14
+ callbacks = []
15
+ callbacks.concat(args)
16
+ callbacks.push(block) if block_given?
17
+ self.send("#{attribute_name}=", callbacks)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ module ::React
25
+ module Callbacks
26
+
27
+ alias_method :original_run_callback, :run_callback
28
+
29
+ def run_callback(name, *args)
30
+ # monkey patch run callback because its easiest place to hook
31
+ # into all components lifecycles.
32
+ React::Component.add_to_global_component_list self if name == :before_mount
33
+ original_run_callback name, *args
34
+ React::Component.remove_from_global_component_list self if name == :before_unmount
35
+ end
36
+
37
+ end
38
+
39
+ module Component
40
+
41
+ def self.add_to_global_component_list instance
42
+ # puts "Adding #{instance} to component list"
43
+ (@global_component_list ||= Set.new).add instance
44
+ end
45
+
46
+ def self.remove_from_global_component_list instance
47
+ # puts "Removing #{instance} from component list"
48
+ @global_component_list.delete instance
49
+ end
50
+
51
+ def self.force_update!
52
+ # puts "Forcing global update"
53
+ @global_component_list && @global_component_list.each(&:force_update!)
54
+ end
55
+
56
+ end
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,133 @@
1
+ require 'native'
2
+ require 'io/writable'
3
+
4
+ class OpalHotReloader
5
+
6
+ # Code taken from opal browser, did not want to force an opal-browser dependency
7
+ #
8
+ # A {Socket} allows the browser and a server to have a bidirectional data
9
+ # connection.
10
+ #
11
+ # @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
12
+ class Socket
13
+ def self.supported?
14
+ Browser.supports? :WebSocket
15
+ end
16
+
17
+ include Native
18
+ include IO::Writable
19
+
20
+ def on(str, &block)
21
+ puts "putting #{str}"
22
+ b = block
23
+ cmd = "foo.on#{str} = #{b}"
24
+ puts cmd
25
+ `#{cmd}`
26
+ # `foo.on#{str} = #{b}`
27
+ end
28
+ # include DOM::Event::Target
29
+
30
+ # target {|value|
31
+ #Socket.new(value) if Native.is_a?(value, `window.WebSocket`)
32
+ # }
33
+
34
+ # Create a connection to the given URL, optionally using the given protocol.
35
+ #
36
+ # @param url [String] the URL to connect to
37
+ # @param protocol [String] the protocol to use
38
+ #
39
+ # @yield if the block has no parameters it's `instance_exec`d, otherwise it's
40
+ # called with `self`
41
+ def initialize(url, protocol = nil, &block)
42
+ if native?(url)
43
+ super(url)
44
+ elsif protocol
45
+ super(`new window.WebSocket(#{url.to_s}, #{protocol.to_n})`)
46
+ else
47
+ super(`new window.WebSocket(#{url.to_s})`)
48
+ end
49
+
50
+ if block.arity == 0
51
+ instance_exec(&block)
52
+ else
53
+ block.call(self)
54
+ end if block
55
+ end
56
+
57
+ # @!attribute [r] protocol
58
+ # @return [String] the protocol of the socket
59
+ alias_native :protocol
60
+
61
+ # @!attribute [r] url
62
+ # @return [String] the URL the socket is connected to
63
+ alias_native :url
64
+
65
+ # @!attribute [r] buffered
66
+ # @return [Integer] the amount of buffered data.
67
+ alias_native :buffered, :bufferedAmount
68
+
69
+ # @!attribute [r] type
70
+ # @return [:blob, :buffer, :string] the type of the socket
71
+ def type
72
+ %x{
73
+ switch (#@native.binaryType) {
74
+ case "blob":
75
+ return "blob";
76
+
77
+ case "arraybuffer":
78
+ return "buffer";
79
+
80
+ default:
81
+ return "string";
82
+ }
83
+ }
84
+ end
85
+
86
+ # @!attribute [r] state
87
+ # @return [:connecting, :open, :closing, :closed] the state of the socket
88
+ def state
89
+ %x{
90
+ switch (#@native.readyState) {
91
+ case window.WebSocket.CONNECTING:
92
+ return "connecting";
93
+
94
+ case window.WebSocket.OPEN:
95
+ return "open";
96
+
97
+ case window.WebSocket.CLOSING:
98
+ return "closing";
99
+
100
+ case window.WebSocket.CLOSED:
101
+ return "closed";
102
+ }
103
+ }
104
+ end
105
+
106
+ # @!attribute [r] extensions
107
+ # @return [Array<String>] the extensions used by the socket
108
+ def extensions
109
+ `#@native.extensions`.split(/\s*,\s*/)
110
+ end
111
+
112
+ # Check if the socket is alive.
113
+ def alive?
114
+ state == :open
115
+ end
116
+
117
+ # Send data to the socket.
118
+ #
119
+ # @param data [#to_n] the data to send
120
+ def write(data)
121
+ `#@native.send(#{data.to_n})`
122
+ end
123
+
124
+ # Close the socket.
125
+ #
126
+ # @param code [Integer, nil] the error code
127
+ # @param reason [String, nil] the reason for closing
128
+ def close(code = nil, reason = nil)
129
+ `#@native.close(#{code.to_n}, #{reason.to_n})`
130
+ end
131
+ end
132
+
133
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'opal_hot_reloader/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "opal_hot_reloader"
8
+ spec.version = OpalHotReloader::VERSION
9
+ spec.authors = ["Forrest Chang"]
10
+ spec.email = ["fchang@hedgeye.com"]
11
+
12
+ spec.summary = %q{Opal Hot reloader}
13
+ spec.description = %q{Opal Hot Reloader with reactrb suppot}
14
+ spec.homepage = "https://github.com/fkchang/opal_hot_reloader"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "bin"
19
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.10"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_development_dependency "rspec"
25
+ spec.add_development_dependency "opal-rspec", "~> 0.5.0"
26
+
27
+
28
+ spec.add_dependency 'listen', '~> 3.0'
29
+ spec.add_dependency 'websocket'
30
+ end
@@ -0,0 +1,82 @@
1
+ require 'native'
2
+ require 'opal_hot_reloader'
3
+ require 'opal_hot_reloader/css_reloader'
4
+ describe OpalHotReloader::CssReloader do
5
+ # Creates a DOM stylesheet link
6
+ # @param href [String] the link url
7
+ def create_link( href)
8
+ %x|
9
+ var ss = document.createElement("link");
10
+ ss.type = "text/css";
11
+ ss.rel = "stylesheet";
12
+ ss.href = #{href};
13
+ return ss;
14
+ |
15
+ end
16
+
17
+ # Creates a document test double and the link to check whether it has been altered right
18
+ # @param href [String] the link url
19
+ def fake_links_document(href)
20
+ link = create_link(href)
21
+ doc = `{ getElementsByTagName: function(name) { links = [ #{link}]; return links;}}`
22
+ { link: link, document: doc}
23
+ end
24
+
25
+
26
+ context 'Rack::Sass::Plugin' do
27
+ it 'should append t_hot_reload to a css path' do
28
+ css_path = 'stylesheets/base.css'
29
+ doubles = fake_links_document(css_path)
30
+ link = Native(doubles[:link])
31
+ expect(link[:href]).to match /#{Regexp.escape(css_path)}$/
32
+ subject.reload({ url: css_path}, doubles[:document])
33
+ expect(link[:href]).to match /#{Regexp.escape(css_path)}\?t_hot_reload=\d+/
34
+ end
35
+
36
+ it 'should update t_hot_reload argument if there is one already' do
37
+ css_path = 'stylesheets/base.css?t_hot_reload=1111111111111'
38
+ doubles = fake_links_document(css_path)
39
+ link = Native(doubles[:link])
40
+ expect(link[:href]).to match /#{Regexp.escape(css_path)}$/
41
+ subject.reload({ url: css_path}, doubles[:document])
42
+ expect(link[:href]).to match /#{Regexp.escape('stylesheets/base.css?t_hot_reload=')}(\d)+/
43
+ expect($1).to_not eq '1111111111111'
44
+ end
45
+
46
+ it 'should append t_hot_reload if there are existing arguments' do
47
+ css_path = 'stylesheets/base.css?some-arg=1'
48
+ doubles = fake_links_document(css_path)
49
+ link = Native(doubles[:link])
50
+ expect(link[:href]).to match /#{Regexp.escape(css_path)}$/
51
+ subject.reload({ url: css_path}, doubles[:document])
52
+ expect(link[:href]).to match /#{Regexp.escape(css_path)}\&t_hot_reload=(\d)+/
53
+ end
54
+ end
55
+
56
+ context "Rails asset pipeline" do
57
+ it 'should append t_hot_reload to a css path' do
58
+ css_path = "http://localhost:8080/assets/company.self-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.css?body=1"
59
+ doubles = fake_links_document(css_path)
60
+ link = Native(doubles[:link])
61
+ expect(link[:href]).to match /#{Regexp.escape(css_path)}$/
62
+ raw_scss_path = "app/assets/stylesheets/company.css.css"
63
+ subject.reload({ url: raw_scss_path}, doubles[:document])
64
+ expect(link[:href]).to match /#{Regexp.escape(css_path)}\&t_hot_reload=\d+/
65
+ end
66
+
67
+ it 'should update t_hot_reload arguments' do
68
+ css_path ="http://localhost:8080/assets/company.self-055b3f2f4bbc772b1161698989ee095020c65e0283f4e732c66153e06b266ca8.css?body=1&t_hot_reload=1464733023"
69
+ doubles = fake_links_document(css_path)
70
+ link = Native(doubles[:link])
71
+ expect(link[:href]).to match /#{Regexp.escape(css_path)}$/
72
+ raw_scss_path = "app/assets/stylesheets/company.css.css"
73
+ subject.reload({ url: raw_scss_path}, doubles[:document])
74
+ if link[:href] =~ /(.+)\&t_hot_reload=(\d+)/
75
+ new_timestamp = $2
76
+ expect(new_timestamp).to_not eq("1464733023")
77
+ else
78
+ fail("new link_path is broken")
79
+ end
80
+ end
81
+ end
82
+ end
metadata ADDED
@@ -0,0 +1,151 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: opal_hot_reloader
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Forrest Chang
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-10-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.10'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: opal-rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.5.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.5.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: listen
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: websocket
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: Opal Hot Reloader with reactrb suppot
98
+ email:
99
+ - fchang@hedgeye.com
100
+ executables:
101
+ - console
102
+ - opal-hot-reloader
103
+ - setup
104
+ extensions: []
105
+ extra_rdoc_files: []
106
+ files:
107
+ - ".gitignore"
108
+ - ".rspec"
109
+ - ".travis.yml"
110
+ - Gemfile
111
+ - LICENSE.txt
112
+ - README.md
113
+ - Rakefile
114
+ - bin/console
115
+ - bin/opal-hot-reloader
116
+ - bin/setup
117
+ - lib/opal_hot_reloader.rb
118
+ - lib/opal_hot_reloader/server.rb
119
+ - lib/opal_hot_reloader/version.rb
120
+ - opal/opal_hot_reloader.rb
121
+ - opal/opal_hot_reloader/css_reloader.rb
122
+ - opal/opal_hot_reloader/foo.rb
123
+ - opal/opal_hot_reloader/reactrb_patches.rb
124
+ - opal/opal_hot_reloader/socket.rb
125
+ - opal_hot_reloader.gemspec
126
+ - spec-opal/opal_hot_reloader/css_reloader_spec.rb
127
+ homepage: https://github.com/fkchang/opal_hot_reloader
128
+ licenses:
129
+ - MIT
130
+ metadata: {}
131
+ post_install_message:
132
+ rdoc_options: []
133
+ require_paths:
134
+ - lib
135
+ required_ruby_version: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ required_rubygems_version: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ requirements: []
146
+ rubyforge_project:
147
+ rubygems_version: 2.5.1
148
+ signing_key:
149
+ specification_version: 4
150
+ summary: Opal Hot reloader
151
+ test_files: []