opal_hot_reloader 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []