ruflet_cli 0.0.5 → 0.0.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 83772c8986411d4680455374f1f350c015183318e3c090426ea53edfb4ed2a89
4
- data.tar.gz: 45f42f969bc487714f24c0833ca8fdbf2236e789622deb604a231d1a254ea483
3
+ metadata.gz: bd07971461f7a7a4f8f61df41b51b4a6537de8b0dfe152fd71c52148a344c15e
4
+ data.tar.gz: 65b0952b26820338f5a4f4115440aac955c0ab09bac94fb3fcc94d19e302eff1
5
5
  SHA512:
6
- metadata.gz: 412a6524bff1cdf335256193390aca08c71b299f517da00af1da6efada84c7fc0ebd0d399be9e92235d6713ebcfbd8c8f9695695b68e3465242ed998deecf1bb
7
- data.tar.gz: 0f77d2ee780f5d443e58d356ac02382ae0e5a53144ac64d9df5bf8d4e9bec2fcf51646f8d92fdc69e527259a7100e772fafcd26861c2d63ba829606531b03824
6
+ metadata.gz: 28b45040c5f0f3b0901d48bedfdd995e644983e3d4e3748696442f21288175722d3b13688f646f4b1cbea662405213b0e6dedf970aa4614b57d9f2000a5e65a8
7
+ data.tar.gz: 891b2e7de50d432d0922f7f8aa0e2d28e764d4c8c02def6a7de5c58fbc26c86b9421687516427c84efd0a431e79d546c698962d8d421a54ddf97cb2f246b99ed
@@ -9,16 +9,19 @@ require "fileutils"
9
9
  require "json"
10
10
  require "net/http"
11
11
  require "uri"
12
+ require "thread"
13
+ require "io/console"
12
14
 
13
15
  module Ruflet
14
16
  module CLI
15
17
  module RunCommand
16
18
  def command_run(args)
17
- options = { target: "mobile" }
19
+ options = { target: "mobile", hot_reload: true }
18
20
  parser = OptionParser.new do |o|
19
21
  o.on("--web") { options[:target] = "web" }
20
- o.on("--mobile") { options[:target] = "mobile" }
21
22
  o.on("--desktop") { options[:target] = "desktop" }
23
+ o.on("--hot-reload", "--hot") { options[:hot_reload] = true }
24
+ o.on("--no-hot-reload") { options[:hot_reload] = false }
22
25
  end
23
26
  parser.parse!(args)
24
27
 
@@ -44,16 +47,8 @@ module Ruflet
44
47
  print_mobile_qr_hint(port: selected_port) if options[:target] == "mobile"
45
48
 
46
49
  gemfile_path = find_nearest_gemfile(Dir.pwd)
47
- cmd =
48
- if gemfile_path
49
- env["BUNDLE_GEMFILE"] = gemfile_path
50
- bundle_ready = system(env, "bundle", "check", out: File::NULL, err: File::NULL)
51
- return 1 unless bundle_ready || system(env, "bundle", "install")
52
-
53
- ["bundle", "exec", RbConfig.ruby, script_path]
54
- else
55
- [RbConfig.ruby, script_path]
56
- end
50
+ cmd = build_runtime_command(script_path, gemfile_path: gemfile_path, env: env, hot_reload: options[:hot_reload])
51
+ return 1 unless cmd
57
52
 
58
53
  child_pid = Process.spawn(env, *cmd, pgroup: true)
59
54
  launched_client_pids = launch_target_client(options[:target], selected_port)
@@ -68,8 +63,22 @@ module Ruflet
68
63
  previous_int = Signal.trap("INT") { forward_signal.call("INT") }
69
64
  previous_term = Signal.trap("TERM") { forward_signal.call("TERM") }
70
65
 
71
- _pid, status = Process.wait2(child_pid)
72
- status.success? ? 0 : (status.exitstatus || 1)
66
+ if options[:hot_reload]
67
+ puts "Hot reload enabled. Press 'r' to reload backend or 'R' for hot restart."
68
+ run_hot_reload_loop(
69
+ child_pid: child_pid,
70
+ env: env,
71
+ cmd: cmd,
72
+ script_path: script_path,
73
+ gemfile_path: gemfile_path,
74
+ target: options[:target],
75
+ port: selected_port,
76
+ launched_client_pids: launched_client_pids
77
+ )
78
+ else
79
+ _pid, status = Process.wait2(child_pid)
80
+ status.success? ? 0 : (status.exitstatus || 1)
81
+ end
73
82
  ensure
74
83
  Signal.trap("INT", previous_int) if defined?(previous_int) && previous_int
75
84
  Signal.trap("TERM", previous_term) if defined?(previous_term) && previous_term
@@ -93,10 +102,186 @@ module Ruflet
93
102
  end
94
103
  end
95
104
  end
105
+
96
106
  end
97
107
 
98
108
  private
99
109
 
110
+ def run_hot_reload_loop(child_pid:, env:, cmd:, script_path:, gemfile_path:, target:, port:, launched_client_pids:)
111
+ hot_queue = Queue.new
112
+ hot_listener = start_hotkey_listener(hot_queue)
113
+ watch_roots = watch_roots_for_script(script_path, gemfile_path)
114
+ watch_snapshot = build_watch_snapshot(watch_roots)
115
+ puts "Watching #{watch_snapshot.size} Ruby files for hot reload."
116
+
117
+ loop do
118
+ exited = Process.waitpid(child_pid, Process::WNOHANG)
119
+ if exited
120
+ status = $?
121
+ return status&.success? ? 0 : (status&.exitstatus || 1)
122
+ end
123
+
124
+ latest_snapshot = build_watch_snapshot(watch_roots)
125
+ if latest_snapshot != watch_snapshot
126
+ changed = changed_paths(watch_snapshot, latest_snapshot)
127
+ sample = changed.first(3).map { |p| File.basename(p) }.join(", ")
128
+ suffix = changed.size > 3 ? ", ..." : ""
129
+ puts "Detected change in #{changed.size} file(s): #{sample}#{suffix}"
130
+ hot_queue << :reload
131
+ watch_snapshot = latest_snapshot
132
+ end
133
+
134
+ process_hot_reload_actions(
135
+ queue: hot_queue,
136
+ child_pid: child_pid,
137
+ launched_client_pids: launched_client_pids,
138
+ env: env,
139
+ cmd: cmd,
140
+ target: target,
141
+ port: port
142
+ ) do |new_child_pid, new_client_pids|
143
+ child_pid = new_child_pid
144
+ launched_client_pids = new_client_pids
145
+ end
146
+
147
+ sleep 0.25
148
+ end
149
+ ensure
150
+ hot_listener&.kill
151
+ end
152
+
153
+ def process_hot_reload_actions(queue:, child_pid:, launched_client_pids:, env:, cmd:, target:, port:)
154
+ until queue.empty?
155
+ action = queue.pop(true)
156
+ case action
157
+ when :interrupt
158
+ Process.kill("INT", Process.pid)
159
+ when :reload
160
+ puts "Reloading backend..."
161
+ child_pid = restart_backend(child_pid, env, cmd)
162
+ when :restart
163
+ puts "Hot restarting backend and client..."
164
+ child_pid = restart_backend(child_pid, env, cmd)
165
+ launched_client_pids = restart_clients(launched_client_pids, target, port)
166
+ end
167
+ yield child_pid, launched_client_pids if block_given?
168
+ end
169
+ rescue ThreadError
170
+ nil
171
+ end
172
+
173
+ def start_hotkey_listener(queue)
174
+ return nil unless $stdin.tty?
175
+
176
+ Thread.new do
177
+ $stdin.raw do |stdin|
178
+ loop do
179
+ key = stdin.getc
180
+ break if key.nil?
181
+
182
+ action = hot_action_for_key(key)
183
+ queue << action if action
184
+ end
185
+ end
186
+ rescue StandardError
187
+ nil
188
+ end
189
+ end
190
+
191
+ def hot_action_for_key(key)
192
+ return :interrupt if key == "\u0003" # Ctrl+C in raw mode
193
+ return :reload if key == "r"
194
+ return :restart if key == "R"
195
+
196
+ nil
197
+ end
198
+
199
+ def restart_backend(child_pid, env, cmd)
200
+ terminate_process(child_pid)
201
+ new_pid = Process.spawn(env, *cmd, pgroup: true)
202
+ puts "Hot reload applied."
203
+ new_pid
204
+ end
205
+
206
+ def build_runtime_command(script_path, gemfile_path:, env:, hot_reload:)
207
+ if gemfile_path
208
+ env["BUNDLE_GEMFILE"] = gemfile_path
209
+ bundle_ready = system(env, RbConfig.ruby, "-S", "bundle", "check", out: File::NULL, err: File::NULL)
210
+ return nil unless bundle_ready || system(env, RbConfig.ruby, "-S", "bundle", "install")
211
+
212
+ return [RbConfig.ruby, "-rbundler/setup", script_path]
213
+ end
214
+
215
+ [RbConfig.ruby, script_path]
216
+ end
217
+
218
+ def restart_clients(launched_client_pids, target, port)
219
+ Array(launched_client_pids).compact.each { |pid| terminate_process(pid) }
220
+ launch_target_client(target, port)
221
+ end
222
+
223
+ def terminate_process(pid)
224
+ return unless pid
225
+
226
+ begin
227
+ Process.kill("TERM", -pid)
228
+ rescue Errno::ESRCH
229
+ begin
230
+ Process.kill("TERM", pid)
231
+ rescue Errno::ESRCH
232
+ nil
233
+ end
234
+ end
235
+
236
+ begin
237
+ Timeout.timeout(2) { Process.wait(pid) }
238
+ rescue StandardError
239
+ nil
240
+ end
241
+ end
242
+
243
+ def watch_roots_for_script(script_path, gemfile_path)
244
+ roots = [Dir.pwd, File.dirname(script_path)]
245
+ if gemfile_path
246
+ roots << File.dirname(gemfile_path)
247
+ roots.concat(extra_watch_roots_from_gemfile(gemfile_path))
248
+ end
249
+ roots.uniq.select { |root| File.directory?(root) }
250
+ end
251
+
252
+ def extra_watch_roots_from_gemfile(gemfile_path)
253
+ base_dir = File.dirname(gemfile_path)
254
+ content = File.read(gemfile_path)
255
+ content.scan(/path:\s*["']([^"']+)["']/).flatten.map do |path|
256
+ File.expand_path(path, base_dir)
257
+ end.select { |path| File.directory?(path) }
258
+ rescue StandardError
259
+ []
260
+ end
261
+
262
+ def build_watch_snapshot(roots)
263
+ snapshot = {}
264
+ roots.each do |root|
265
+ Dir.glob(File.join(root, "**", "*.rb")).each do |path|
266
+ next if path.include?("/.git/")
267
+ next if path.include?("/tmp/")
268
+ next if path.include?("/log/")
269
+ next unless File.file?(path)
270
+
271
+ snapshot[path] = File.mtime(path).to_f
272
+ rescue Errno::ENOENT
273
+ nil
274
+ end
275
+ end
276
+ snapshot
277
+ end
278
+
279
+ def changed_paths(old_snapshot, new_snapshot)
280
+ (old_snapshot.keys | new_snapshot.keys).select do |path|
281
+ old_snapshot[path] != new_snapshot[path]
282
+ end
283
+ end
284
+
100
285
  def resolve_script(token)
101
286
  path = File.expand_path(token, Dir.pwd)
102
287
  return path if File.file?(path)
@@ -547,7 +732,7 @@ module Ruflet
547
732
  end
548
733
  probe.close
549
734
  return port
550
- rescue Errno::EADDRINUSE
735
+ rescue Errno::EADDRINUSE, Errno::EACCES, Errno::EPERM
551
736
  port += 1
552
737
  end
553
738
  end
@@ -556,13 +741,7 @@ module Ruflet
556
741
  end
557
742
 
558
743
  def resolve_backend_port(target)
559
- return find_available_port(8550) if target == "mobile"
560
-
561
- return 8550 if port_available?(8550)
562
-
563
- warn "Port 8550 is required for `ruflet run --#{target}`."
564
- warn "Stop the process using 8550 and run again."
565
- nil
744
+ find_available_port(8550)
566
745
  end
567
746
 
568
747
  def port_available?(port)
@@ -3,58 +3,42 @@
3
3
  module Ruflet
4
4
  module CLI
5
5
  MAIN_TEMPLATE = <<~RUBY
6
- require "ruflet"
7
-
8
- class MainApp < Ruflet::App
9
- def initialize
10
- super
11
- @count = 0
12
- end
13
-
14
- def view(page)
15
- page.title = "Counter Demo"
16
- page.vertical_alignment = Ruflet::MainAxisAlignment::CENTER
17
- page.horizontal_alignment = Ruflet::CrossAxisAlignment::CENTER
18
- count_text = text(value: @count.to_s, size: 40)
19
-
20
- page.add(
21
- container(
22
- expand: true,
23
- padding: 24,
24
- content: column(
25
- expand: true,
26
- alignment: Ruflet::MainAxisAlignment::CENTER,
27
- horizontal_alignment: Ruflet::CrossAxisAlignment::CENTER,
28
- spacing: 12,
29
- controls: [
30
- text(value: "You have pushed the button this many times:"),
31
- count_text
32
- ]
33
- )
34
- ),
35
- appbar: app_bar(
36
- title: text(value: "Counter Demo")
37
- ),
38
- floating_action_button: fab(
39
- icon(icon: Ruflet::MaterialIcons::ADD),
40
- on_click: ->(_e) {
41
- @count += 1
42
- page.update(count_text, value: @count.to_s)
43
- }
44
- )
6
+ require "ruflet"
7
+ Ruflet.run do |page|
8
+ page.title = "Counter Demo"
9
+ count = 0
10
+ count_text = nil
11
+ count_text ||= text(value: count.to_s, size: 40)
12
+ page.add(
13
+ container(
14
+ expand: true,
15
+ alignment: Ruflet::MainAxisAlignment::CENTER,
16
+ content: column(
17
+ alignment: Ruflet::MainAxisAlignment::CENTER,
18
+ horizontal_alignment: Ruflet::CrossAxisAlignment::CENTER,
19
+ children: [
20
+ text(value: "You have pushed the button this many times:"),
21
+ count_text
22
+ ]
45
23
  )
46
- end
47
- end
48
-
49
- MainApp.new.run
24
+ ),
25
+ floating_action_button: fab(
26
+ icon(icon: Ruflet::MaterialIcons::ADD),
27
+ on_click: ->(_e) do
28
+ count += 1
29
+ page.update(count_text, value: count.to_s)
30
+ end
31
+ )
32
+ )
33
+ end
50
34
 
51
35
  RUBY
52
36
 
53
37
  GEMFILE_TEMPLATE = <<~GEMFILE
54
38
  source "https://rubygems.org"
55
39
 
56
- gem "ruflet", ">= 0.0.3"
57
- gem "ruflet_server", ">= 0.0.3"
40
+ gem "ruflet", ">= 0.0.7"
41
+ gem "ruflet_server", ">= 0.0.7"
58
42
  GEMFILE
59
43
 
60
44
  README_TEMPLATE = <<~MD
data/lib/ruflet/cli.rb CHANGED
@@ -54,7 +54,7 @@ module Ruflet
54
54
  Commands:
55
55
  ruflet create <appname>
56
56
  ruflet new <appname>
57
- ruflet run [scriptname|path] [--web|--mobile|--desktop]
57
+ ruflet run [scriptname|path] [--web|--desktop] [--no-hot-reload]
58
58
  ruflet debug [scriptname|path]
59
59
  ruflet build <apk|ios|aab|web|macos|windows|linux>
60
60
  ruflet devices
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ruflet
4
- VERSION = "0.0.5" unless const_defined?(:VERSION)
4
+ VERSION = "0.0.7" unless const_defined?(:VERSION)
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruflet_cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - AdamMusa