ruflet 0.0.19 → 0.0.20

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.
@@ -131,7 +131,21 @@ module Ruflet
131
131
  end
132
132
 
133
133
  def fvm_env
134
- { "PATH" => "#{pub_cache_bin_dir}#{File::PATH_SEPARATOR}#{ENV.fetch('PATH', '')}" }
134
+ utf8_locale_env.merge("PATH" => "#{pub_cache_bin_dir}#{File::PATH_SEPARATOR}#{ENV.fetch('PATH', '')}")
135
+ end
136
+
137
+ # CocoaPods aborts with "Unicode Normalization not appropriate for
138
+ # ASCII-8BIT" when it inherits a non-UTF-8 locale, which takes the whole
139
+ # iOS and macOS build with it. Hand the toolchain a UTF-8 locale unless
140
+ # the environment already has one.
141
+ def utf8_locale_env
142
+ return {} if utf8_locale?(ENV["LC_ALL"]) || utf8_locale?(ENV["LANG"])
143
+
144
+ { "LANG" => "en_US.UTF-8", "LC_ALL" => "en_US.UTF-8" }
145
+ end
146
+
147
+ def utf8_locale?(value)
148
+ value.to_s.match?(/utf-?8/i)
135
149
  end
136
150
 
137
151
  def tools_from_flutter_bin(flutter_bin)
@@ -143,11 +157,11 @@ module Ruflet
143
157
  {
144
158
  flutter: flutter_bin,
145
159
  dart: (File.executable?(dart) ? dart : "dart"),
146
- env: {
160
+ env: utf8_locale_env.merge(
147
161
  "PATH" => "#{bin_dir}#{File::PATH_SEPARATOR}#{pub_cache_bin_dir}#{File::PATH_SEPARATOR}#{ENV.fetch('PATH', '')}",
148
162
  "FLUTTER_ROOT" => sdk_root,
149
163
  "FVM_FLUTTER_SDK" => sdk_root
150
- }
164
+ )
151
165
  }
152
166
  end
153
167
 
@@ -25,6 +25,7 @@ module Ruflet
25
25
  "lottie" => { package: "flet_lottie", alias: "ruflet_lottie" },
26
26
  "map" => { package: "flet_map", alias: "ruflet_map" },
27
27
  "permission_handler" => { package: "flet_permission_handler", alias: "ruflet_permission_handler" },
28
+ "qrcode_scanner" => { package: "ruflet_qrcode_scanner", alias: "ruflet_qrcode_scanner" },
28
29
  "rive" => { package: "flet_rive", alias: "ruflet_rive" },
29
30
  "secure_storage" => { package: "flet_secure_storage", alias: "ruflet_secure_storage" },
30
31
  "video" => { package: "flet_video", alias: "ruflet_video" },
@@ -54,11 +55,11 @@ module Ruflet
54
55
  puts "Run:"
55
56
  puts " cd #{project_name}"
56
57
  puts " bundle install"
57
- puts " bundle exec ruflet run main.rb"
58
+ puts " ruflet run main.rb"
58
59
  puts
59
60
  puts "Build:"
60
- puts " bundle exec ruflet build android --self"
61
- puts " bundle exec ruflet build ios --self"
61
+ puts " ruflet build android --self"
62
+ puts " ruflet build ios --self"
62
63
  0
63
64
  end
64
65
 
@@ -81,21 +82,23 @@ module Ruflet
81
82
  end
82
83
 
83
84
  def resolve_ruflet_client_template_root
84
- [
85
- File.expand_path("../../../../../templates/ruflet_flutter_template", __dir__)
86
- ].each do |template|
87
- return template if Dir.exist?(template)
85
+ explicit = ENV["RUFLET_TEMPLATE_ROOT"].to_s.strip
86
+ unless explicit.empty?
87
+ explicit_template = [
88
+ explicit,
89
+ File.join(explicit, "templates", "ruflet_flutter_template")
90
+ ].find { |path| valid_ruflet_template?(path) }
91
+ return explicit_template if explicit_template
88
92
  end
89
93
 
90
- cached_template = cached_ruflet_client_template_root
91
- return cached_template if Dir.exist?(cached_template)
94
+ sibling_template = File.expand_path(
95
+ "../../../../../../ruflet-template/templates/ruflet_flutter_template",
96
+ __dir__
97
+ )
98
+ return sibling_template if valid_ruflet_template?(sibling_template)
92
99
 
93
- [
94
- File.expand_path("../../../ruflet_client", __dir__),
95
- File.expand_path("../../../../../ruflet_client", __dir__)
96
- ].each do |fallback|
97
- return fallback if Dir.exist?(fallback)
98
- end
100
+ cached_template = cached_ruflet_client_template_root
101
+ return cached_template if valid_ruflet_template?(cached_template)
99
102
 
100
103
  nil
101
104
  end
@@ -255,12 +258,6 @@ module Ruflet
255
258
  def write_default_ruflet_config(root, app_name)
256
259
  File.write(File.join(root, "ruflet.yaml"), <<~YAML)
257
260
  app:
258
- name: #{app_name}
259
- display_name: #{humanize_name(app_name)}
260
- package_name: #{app_name.gsub(/[^a-zA-Z0-9_]+/, "_").downcase}
261
- organization: com.example
262
- version: 1.0.0+1
263
- description: A new Ruflet app.
264
261
  # Required for server-driven builds: `ruflet build ios`, `apk`, `web`, etc. without `--self`.
265
262
  # Example: https://api.example.com
266
263
  backend_url: ""
@@ -284,6 +281,16 @@ module Ruflet
284
281
  YAML
285
282
 
286
283
  File.write(File.join(root, "services.yaml"), <<~YAML)
284
+ # Application identity used by the Flutter build pipeline. Ruflet derives
285
+ # the mobile identifier as organization.package_name and passes it to
286
+ # change_app_package_name for Android and iOS builds.
287
+ app:
288
+ app_name: #{humanize_name(app_name)}
289
+ package_name: #{app_name.gsub(/[^a-zA-Z0-9_]+/, "_").downcase}
290
+ organization: com.example
291
+ version: 1.0.0+1
292
+ description: A new Ruflet app.
293
+
287
294
  # Native capabilities that require platform permissions. Ruflet activates
288
295
  # the matching client extensions and writes Android/iOS permissions.
289
296
  # Supported services: camera, microphone, location, motion
@@ -20,11 +20,12 @@ module Ruflet
20
20
  DEFAULT_CLIENT_UPDATE_INTERVAL = 6 * 60 * 60
21
21
 
22
22
  def command_run(args)
23
- options = { target: "mobile", requested_port: 8550 }
23
+ options = { target: "mobile", requested_port: 8550, reload: true }
24
24
  parser = OptionParser.new do |o|
25
25
  o.on("--web") { options[:target] = "web" }
26
26
  o.on("--desktop") { options[:target] = "desktop" }
27
27
  o.on("--port PORT", Integer) { |v| options[:requested_port] = v }
28
+ o.on("--no-reload") { options[:reload] = false }
28
29
  end
29
30
  parser.parse!(args)
30
31
 
@@ -47,18 +48,32 @@ module Ruflet
47
48
  assets_dir = File.join(File.dirname(script_path), "assets")
48
49
  env["RUFLET_ASSETS_DIR"] = assets_dir if File.directory?(assets_dir)
49
50
 
51
+ # The backend serves the web client itself so both share one origin.
52
+ if options[:target] == "web"
53
+ web_client_dir = detect_web_client_dir
54
+ if web_client_dir
55
+ env["RUFLET_WEB_CLIENT_DIR"] = web_client_dir
56
+ else
57
+ warn "Web client build not found and prebuilt download failed."
58
+ warn "Build one with `ruflet build web`, or set RUFLET_CLIENT_DIR."
59
+ return 1
60
+ end
61
+ end
62
+
50
63
  print_run_banner(target: options[:target], requested_port: options[:requested_port], port: selected_port)
51
64
  print_mobile_qr_hint(port: selected_port) if options[:target] == "mobile"
65
+ print_hot_reload_banner if options[:reload]
52
66
 
53
67
  gemfile_path = find_nearest_gemfile(Dir.pwd)
54
- cmd = build_runtime_command(script_path, gemfile_path: gemfile_path, env: env)
68
+ cmd = build_runtime_command(script_path, gemfile_path: gemfile_path, env: env, reload: options[:reload])
55
69
  return 1 unless cmd
56
70
 
57
- child_pid = Process.spawn(env, *cmd, pgroup: true)
71
+ run_state = { child_pid: Process.spawn(env, *cmd, pgroup: true), restart: false }
72
+ reload_input_thread = options[:reload] ? start_reload_input_thread(run_state) : nil
58
73
  launched_client_pids = launch_target_client(options[:target], selected_port)
59
74
  forward_signal = lambda do |signal|
60
75
  begin
61
- Process.kill(signal, -child_pid)
76
+ Process.kill(signal, -run_state[:child_pid])
62
77
  rescue Errno::ESRCH
63
78
  nil
64
79
  end
@@ -67,15 +82,28 @@ module Ruflet
67
82
  previous_int = Signal.trap("INT") { forward_signal.call("INT") }
68
83
  previous_term = Signal.trap("TERM") { forward_signal.call("TERM") }
69
84
 
70
- _pid, status = Process.wait2(child_pid)
71
- status.success? ? 0 : (status.exitstatus || 1)
85
+ loop do
86
+ _pid, status = Process.wait2(run_state[:child_pid])
87
+ return status.success? ? 0 : (status.exitstatus || 1) unless run_state[:restart]
88
+
89
+ # Full restart requested ("R"): respawn the backend; connected
90
+ # clients reconnect and re-register on their own (Flet-style).
91
+ run_state[:restart] = false
92
+ puts "Restarting app..."
93
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
94
+ run_state[:child_pid] = Process.spawn(env, *cmd, pgroup: true)
95
+ wait_for_server_boot(selected_port)
96
+ elapsed_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).round
97
+ puts "Restarted in #{elapsed_ms}ms"
98
+ end
72
99
  ensure
100
+ reload_input_thread&.kill if defined?(reload_input_thread)
73
101
  Signal.trap("INT", previous_int) if defined?(previous_int) && previous_int
74
102
  Signal.trap("TERM", previous_term) if defined?(previous_term) && previous_term
75
103
 
76
- if defined?(child_pid) && child_pid
104
+ if defined?(run_state) && run_state && run_state[:child_pid]
77
105
  begin
78
- Process.kill("TERM", -child_pid)
106
+ Process.kill("TERM", -run_state[:child_pid])
79
107
  rescue Errno::ESRCH
80
108
  nil
81
109
  end
@@ -97,16 +125,111 @@ module Ruflet
97
125
 
98
126
  private
99
127
 
100
- def build_runtime_command(script_path, gemfile_path:, env:)
128
+ def build_runtime_command(script_path, gemfile_path:, env:, reload: false)
129
+ entry_script = script_path
130
+ if reload
131
+ env["RUFLET_APP_SCRIPT"] = script_path
132
+ env["RUFLET_WATCH_ROOT"] = File.dirname(script_path)
133
+ env["RUFLET_BOOTSNAP_DIR"] = bootsnap_cache_dir(script_path)
134
+ entry_script = hot_reload_harness_path
135
+ end
136
+
101
137
  if gemfile_path
102
138
  env["BUNDLE_GEMFILE"] = gemfile_path
103
139
  bundle_ready = system(env, RbConfig.ruby, "-S", "bundle", "check", out: File::NULL, err: File::NULL)
104
140
  return nil unless bundle_ready || system(env, RbConfig.ruby, "-S", "bundle", "install")
105
141
 
106
- return [RbConfig.ruby, "-rbundler/setup", script_path]
142
+ return [RbConfig.ruby, "-rbundler/setup", entry_script]
107
143
  end
108
144
 
109
- [RbConfig.ruby, script_path]
145
+ [RbConfig.ruby, entry_script]
146
+ end
147
+
148
+ def hot_reload_harness_path
149
+ File.expand_path("../hot_reload/harness.rb", __dir__)
150
+ end
151
+
152
+ # Persistent, per-app bootsnap cache so it stays warm across restarts.
153
+ # Kept under ~/.ruflet (not the project, so nothing to gitignore) and
154
+ # keyed by app path + Ruby version so bytecode never crosses apps or
155
+ # incompatible VMs.
156
+ def bootsnap_cache_dir(script_path)
157
+ app_key = File.dirname(File.expand_path(script_path)).gsub(/[^a-zA-Z0-9]+/, "-").delete_prefix("-")
158
+ File.join(Dir.home, ".ruflet", "bootsnap", RUBY_VERSION, app_key)
159
+ end
160
+
161
+ def manual_reload_supported?
162
+ $stdin.tty? && Signal.list.key?("USR1")
163
+ end
164
+
165
+ def print_hot_reload_banner
166
+ hint = manual_reload_supported? ? "; press \"r\" to reload, \"R\" to restart" : ""
167
+ puts "Hot reload: watching *.rb#{hint} (disable with --no-reload)"
168
+ end
169
+
170
+ def start_reload_input_thread(run_state)
171
+ return nil unless manual_reload_supported?
172
+
173
+ Thread.new do
174
+ loop do
175
+ key = read_reload_key
176
+ break if key.nil?
177
+ break unless handle_reload_command(key, run_state)
178
+ end
179
+ rescue StandardError
180
+ nil
181
+ end
182
+ end
183
+
184
+ # Single keypress, no Enter required. getch puts the terminal in raw
185
+ # mode only for the duration of the read; intr keeps Ctrl-C working.
186
+ def read_reload_key
187
+ $stdin.getch(intr: true)
188
+ rescue ArgumentError
189
+ # Older Rubies without the intr keyword.
190
+ $stdin.getch
191
+ rescue StandardError
192
+ nil
193
+ end
194
+
195
+ # Returns false when the child process is gone and the thread should end.
196
+ def handle_reload_command(command, run_state)
197
+ case command
198
+ when "r"
199
+ Process.kill("USR1", run_state[:child_pid])
200
+ when "R"
201
+ run_state[:restart] = true
202
+ Process.kill("TERM", -run_state[:child_pid])
203
+ escalate_child_shutdown(run_state[:child_pid])
204
+ end
205
+ true
206
+ rescue Errno::ESRCH
207
+ run_state[:restart] = false
208
+ false
209
+ end
210
+
211
+ # An app can block TERM (bad traps, stuck threads); force the restart
212
+ # through with KILL if the child is still running after the grace period.
213
+ def escalate_child_shutdown(child_pid, grace_seconds: 3)
214
+ Thread.new do
215
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + grace_seconds
216
+ loop do
217
+ sleep 0.1
218
+ begin
219
+ Process.kill(0, child_pid)
220
+ rescue Errno::ESRCH
221
+ break
222
+ end
223
+ next unless Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
224
+
225
+ begin
226
+ Process.kill("KILL", -child_pid)
227
+ rescue Errno::ESRCH
228
+ nil
229
+ end
230
+ break
231
+ end
232
+ end
110
233
  end
111
234
 
112
235
  def apply_local_ruflet_dev_overrides(env)
@@ -177,34 +300,24 @@ module Ruflet
177
300
  end
178
301
  end
179
302
 
303
+ # The backend serves the web client on its own port, so the client loads
304
+ # and opens its websocket on the same origin. The explicit url parameter
305
+ # is kept because prebuilt production clients have no default URL and use
306
+ # it to select the websocket transport.
180
307
  def launch_web_client(port)
181
- web_dir = detect_web_client_dir
182
- unless web_dir
183
- warn "Web client build not found and prebuilt download failed."
184
- return []
185
- end
186
-
187
- web_port = find_available_port(port + 1)
188
- web_pid = Process.spawn("python3", "-m", "http.server", web_port.to_s, "--bind", "127.0.0.1", chdir: web_dir, out: File::NULL, err: File::NULL)
189
- Process.detach(web_pid)
190
- wait_for_server_boot(web_port)
191
308
  backend_url = "http://localhost:#{port}"
192
- web_url = "http://localhost:#{web_port}/?#{URI.encode_www_form(url: backend_url)}"
309
+ web_url = "#{backend_url}/?#{URI.encode_www_form(url: backend_url)}"
193
310
  browser_pid = open_in_browser_app_mode(web_url)
194
311
  open_in_browser(web_url) if browser_pid.nil?
195
312
  puts "Ruflet web client: #{web_url}"
196
313
  puts "Ruflet backend ws: ws://localhost:#{port}/ws"
197
- [web_pid, browser_pid].compact
198
- rescue Errno::ENOENT
199
- warn "python3 is required to host web client locally."
200
- warn "Install Python 3 and rerun."
201
- []
314
+ [browser_pid].compact
202
315
  rescue StandardError => e
203
316
  warn "Failed to launch web client: #{e.class}: #{e.message}"
204
317
  []
205
318
  end
206
319
 
207
- def wait_for_server_boot(port, timeout_seconds: 10)
320
+ def wait_for_server_boot(port, timeout_seconds: 10, poll_interval: 0.01)
208
321
  Timeout.timeout(timeout_seconds) do
209
322
  loop do
210
323
  begin
@@ -213,7 +326,7 @@ module Ruflet
213
326
  sock.close
214
327
  break
215
328
  rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
216
- sleep 0.15
329
+ sleep poll_interval
217
330
  end
218
331
  end
219
332
  end
@@ -296,7 +409,10 @@ module Ruflet
296
409
  return
297
410
  end
298
411
 
299
- pid = Process.spawn(*cmd, out: File::NULL, err: File::NULL)
412
+ # The Flutter client took the URL as argv; a Ruflet app used as the
413
+ # client reads RUFLET_URL, so it connects instead of showing its own
414
+ # launcher. Pass both so either client auto-connects.
415
+ pid = Process.spawn({ "RUFLET_URL" => url }, *cmd, out: File::NULL, err: File::NULL)
300
416
  Process.detach(pid)
301
417
  if !pid
302
418
  warn "Failed to launch desktop client: #{cmd.first}"
@@ -309,6 +425,38 @@ module Ruflet
309
425
  []
310
426
  end
311
427
 
428
+ # A Ruflet app used as the preview client builds its native project into
429
+ # build/client, so its Flutter output is one level deeper than a bare
430
+ # Flutter client's. Search both, and never assume the app is named
431
+ # ruflet_client: the bundle takes the app's own display name.
432
+ def client_build_roots(root)
433
+ [File.join(root, "build", "client"), root].select { |dir| Dir.exist?(dir) }
434
+ end
435
+
436
+ def executable_file?(path)
437
+ File.file?(path) && File.executable?(path)
438
+ end
439
+
440
+ # A Flutter project's own web/ folder holds a source index.html, so
441
+ # index.html alone would match an unbuilt project. Require a compiled
442
+ # entrypoint as well.
443
+ WEB_BUILD_MARKERS = %w[flutter_bootstrap.js main.dart.js flutter.js].freeze
444
+
445
+ def built_web_client_dir?(dir)
446
+ return false unless Dir.exist?(dir) && File.file?(File.join(dir, "index.html"))
447
+
448
+ WEB_BUILD_MARKERS.any? { |marker| File.file?(File.join(dir, marker)) }
449
+ end
450
+
451
+ def macos_app_executable(app_bundle)
452
+ macos_dir = File.join(app_bundle, "Contents", "MacOS")
453
+ return nil unless Dir.exist?(macos_dir)
454
+
455
+ Dir.children(macos_dir)
456
+ .map { |entry| File.join(macos_dir, entry) }
457
+ .find { |path| executable_file?(path) }
458
+ end
459
+
312
460
  def detect_desktop_client_command(url)
313
461
  root = ENV["RUFLET_CLIENT_DIR"]
314
462
  root = File.expand_path("ruflet_client", Dir.pwd) if root.to_s.strip.empty?
@@ -317,28 +465,42 @@ module Ruflet
317
465
  return nil unless root && Dir.exist?(root)
318
466
 
319
467
  host_os = RbConfig::CONFIG["host_os"]
320
- if host_os.match?(/darwin/i)
321
- release_bin = File.join(root, "build", "macos", "Build", "Products", "Release", "ruflet_client.app", "Contents", "MacOS", "ruflet_client")
322
- debug_bin = File.join(root, "build", "macos", "Build", "Products", "Debug", "ruflet_client.app", "Contents", "MacOS", "ruflet_client")
323
- prebuilt_bin = File.join(root, "desktop", "ruflet_client.app", "Contents", "MacOS", "ruflet_client")
324
- executable = [release_bin, debug_bin].find { |p| File.file?(p) && File.executable?(p) }
325
- executable ||= prebuilt_bin if File.file?(prebuilt_bin) && File.executable?(prebuilt_bin)
326
- return [executable, url] if executable
327
- elsif host_os.match?(/mswin|mingw|cygwin/i)
328
- exe = File.join(root, "build", "windows", "x64", "runner", "Release", "ruflet_client.exe")
329
- prebuilt = File.join(root, "desktop", "ruflet_client.exe")
330
- exe = prebuilt if !File.file?(exe) && File.file?(prebuilt)
331
- return [exe, url] if File.file?(exe)
332
- else
333
- direct = File.join(root, "build", "linux", "x64", "release", "bundle", "ruflet_client")
334
- prebuilt_direct = File.join(root, "desktop", "ruflet_client")
335
- direct = prebuilt_direct if !File.file?(direct) && File.file?(prebuilt_direct)
336
- return [direct, url] if File.file?(direct)
337
- bundle_dir = File.join(root, "build", "linux", "x64", "release", "bundle")
338
- if Dir.exist?(bundle_dir)
339
- candidate = Dir.children(bundle_dir).map { |f| File.join(bundle_dir, f) }
340
- .find { |path| File.file?(path) && File.executable?(path) }
341
- return [candidate, url] if candidate
468
+ client_build_roots(root).each do |base|
469
+ if host_os.match?(/darwin/i)
470
+ search = %w[Release Debug].map { |config| File.join(base, "build", "macos", "Build", "Products", config) }
471
+ search << File.join(base, "desktop")
472
+ search.each do |dir|
473
+ next unless Dir.exist?(dir)
474
+
475
+ Dir.glob(File.join(dir, "*.app")).sort.each do |app_bundle|
476
+ executable = macos_app_executable(app_bundle)
477
+ return [executable, url] if executable
478
+ end
479
+ end
480
+ elsif host_os.match?(/mswin|mingw|cygwin/i)
481
+ search = [
482
+ File.join(base, "build", "windows", "x64", "runner", "Release"),
483
+ File.join(base, "desktop")
484
+ ]
485
+ search.each do |dir|
486
+ next unless Dir.exist?(dir)
487
+
488
+ exe = Dir.glob(File.join(dir, "*.exe")).sort.find { |path| File.file?(path) }
489
+ return [exe, url] if exe
490
+ end
491
+ else
492
+ search = [
493
+ File.join(base, "build", "linux", "x64", "release", "bundle"),
494
+ File.join(base, "desktop")
495
+ ]
496
+ search.each do |dir|
497
+ next unless Dir.exist?(dir)
498
+
499
+ candidate = Dir.children(dir).sort
500
+ .map { |entry| File.join(dir, entry) }
501
+ .find { |path| executable_file?(path) }
502
+ return [candidate, url] if candidate
503
+ end
342
504
  end
343
505
  end
344
506
 
@@ -352,10 +514,11 @@ module Ruflet
352
514
  root ||= ensure_prebuilt_client(web: true)
353
515
  return nil unless root && Dir.exist?(root)
354
516
 
355
- built = File.join(root, "build", "web")
356
- return built if Dir.exist?(built) && File.file?(File.join(built, "index.html"))
357
- prebuilt = File.join(root, "web")
358
- return prebuilt if Dir.exist?(prebuilt) && File.file?(File.join(prebuilt, "index.html"))
517
+ client_build_roots(root).each do |base|
518
+ [File.join(base, "build", "web"), File.join(base, "web")].each do |dir|
519
+ return dir if built_web_client_dir?(dir)
520
+ end
521
+ end
359
522
 
360
523
  nil
361
524
  end
@@ -7,7 +7,10 @@ module Ruflet
7
7
  Ruflet.run do |page|
8
8
  page.title = "Counter Demo"
9
9
  count = 0
10
- count_text = text(count.to_s, style: {size: 40})
10
+ count_text = text(
11
+ value: count.to_s,
12
+ style: { size: 40, weight: "w700" }
13
+ )
11
14
  page.floating_action_button = fab(
12
15
  icon: "add",
13
16
  on_click: ->(_e) do
@@ -23,7 +26,7 @@ module Ruflet
23
26
  alignment: Ruflet::MainAxisAlignment::CENTER,
24
27
  horizontal_alignment: Ruflet::CrossAxisAlignment::CENTER,
25
28
  children: [
26
- text("You have pushed the button this many times:"),
29
+ text(value: "You have pushed the button this many times:"),
27
30
  count_text
28
31
  ]
29
32
  )
@@ -54,14 +57,14 @@ module Ruflet
54
57
  ## Run
55
58
 
56
59
  ```bash
57
- bundle exec ruflet run main
60
+ ruflet run main
58
61
  ```
59
62
 
60
63
  ## Build
61
64
 
62
65
  ```bash
63
- bundle exec ruflet build apk
64
- bundle exec ruflet build ios
66
+ ruflet build apk
67
+ ruflet build ios
65
68
  ```
66
69
  MD
67
70
  end
data/lib/ruflet/cli.rb CHANGED
@@ -66,7 +66,7 @@ module Ruflet
66
66
  ruflet --version
67
67
  ruflet create <appname>
68
68
  ruflet new <appname>
69
- ruflet run [scriptname|path] [--web|--desktop] [--port PORT]
69
+ ruflet run [scriptname|path] [--web|--desktop] [--port PORT] [--no-reload]
70
70
  ruflet update [web|desktop|all] [--check] [--force] [--platform PLATFORM]
71
71
  ruflet debug [scriptname|path]
72
72
  ruflet build <apk|android|ios|aab|web|macos|windows|linux> [--self] [--verbose]
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Child-process entrypoint used by `ruflet run` when hot reload is enabled.
4
+ # The CLI spawns `ruby [-rbundler/setup] harness.rb` with:
5
+ # RUFLET_APP_SCRIPT absolute path to the application script (required)
6
+ # RUFLET_WATCH_ROOT directory to watch for *.rb changes (optional)
7
+ # RUFLET_BOOTSNAP_DIR bootsnap cache directory (optional)
8
+
9
+ # Bootsnap caches compiled bytecode and $LOAD_PATH resolution, which cuts the
10
+ # cold-boot cost paid on every full restart ("R"). It is optional: used only
11
+ # when the app bundles it (add `gem "bootsnap"` to speed restarts up). Set up
12
+ # before requiring the framework so those requires hit the cache.
13
+ if (cache_dir = ENV["RUFLET_BOOTSNAP_DIR"].to_s) && !cache_dir.empty?
14
+ begin
15
+ require "bootsnap"
16
+ Bootsnap.setup(
17
+ cache_dir: cache_dir,
18
+ load_path_cache: true,
19
+ compile_cache_iseq: true,
20
+ compile_cache_yaml: false
21
+ )
22
+ rescue LoadError
23
+ # bootsnap not in the bundle; boot without it.
24
+ end
25
+ end
26
+
27
+ require_relative "../hot_reload"
28
+
29
+ script = ENV["RUFLET_APP_SCRIPT"].to_s
30
+ abort "ruflet hot reload: RUFLET_APP_SCRIPT is not set" if script.empty?
31
+
32
+ Ruflet::HotReload.run(script: script, watch_root: ENV["RUFLET_WATCH_ROOT"])