ruflet 0.0.21 → 0.0.23
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 +4 -4
- data/README.md +20 -1
- data/lib/ruflet/cli/build_command.rb +1337 -87
- data/lib/ruflet/cli/new_command.rb +65 -17
- data/lib/ruflet/cli/run_command.rb +65 -33
- data/lib/ruflet/cli.rb +6 -1
- data/lib/ruflet/hot_reload.rb +23 -10
- data/lib/ruflet/version.rb +1 -1
- metadata +1 -1
|
@@ -13,23 +13,25 @@ module Ruflet
|
|
|
13
13
|
RUNTIME_REPO_URL = ENV.fetch("RUFLET_RUNTIME_REPO_URL", "https://github.com/AdamMusa/ruflet.git")
|
|
14
14
|
|
|
15
15
|
CLIENT_EXTENSION_MAP = {
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
16
|
+
"ads" => { package: "ruflet_ads", alias: "ruflet_ads" },
|
|
17
|
+
"audio" => { package: "ruflet_audio", alias: "ruflet_audio" },
|
|
18
|
+
"audio_recorder" => { package: "ruflet_audio_recorder", alias: "ruflet_audio_recorder" },
|
|
19
|
+
"camera" => { package: "ruflet_camera", alias: "ruflet_camera" },
|
|
20
|
+
"charts" => { package: "ruflet_charts", alias: "ruflet_charts" },
|
|
21
|
+
"code_editor" => { package: "ruflet_code_editor", alias: "ruflet_code_editor" },
|
|
22
|
+
"color_pickers" => { package: "ruflet_color_pickers", alias: "ruflet_color_picker" },
|
|
23
|
+
"datatable2" => { package: "ruflet_datatable2", alias: "ruflet_datatable2" },
|
|
24
|
+
"flashlight" => { package: "ruflet_flashlight", alias: "ruflet_flashlight" },
|
|
25
|
+
"geolocator" => { package: "ruflet_geolocator", alias: "ruflet_geolocator" },
|
|
26
|
+
"lottie" => { package: "ruflet_lottie", alias: "ruflet_lottie" },
|
|
27
|
+
"map" => { package: "ruflet_map", alias: "ruflet_map" },
|
|
28
|
+
"permission_handler" => { package: "ruflet_permission_handler", alias: "ruflet_permission_handler" },
|
|
28
29
|
"qrcode_scanner" => { package: "ruflet_qrcode_scanner", alias: "ruflet_qrcode_scanner" },
|
|
29
|
-
"rive" => { package: "
|
|
30
|
-
"secure_storage" => { package: "
|
|
31
|
-
"
|
|
32
|
-
"
|
|
30
|
+
"rive" => { package: "ruflet_rive", alias: "ruflet_rive" },
|
|
31
|
+
"secure_storage" => { package: "ruflet_secure_storage", alias: "ruflet_secure_storage" },
|
|
32
|
+
"spinkit" => { package: "ruflet_spinkit", alias: "ruflet_spinkit" },
|
|
33
|
+
"video" => { package: "ruflet_video", alias: "ruflet_video" },
|
|
34
|
+
"webview" => { package: "ruflet_webview", alias: "ruflet_webview" }
|
|
33
35
|
}.freeze
|
|
34
36
|
|
|
35
37
|
def command_new(args)
|
|
@@ -72,7 +74,16 @@ module Ruflet
|
|
|
72
74
|
target = hidden_flutter_client_dir(root)
|
|
73
75
|
FileUtils.mkdir_p(File.dirname(target))
|
|
74
76
|
FileUtils.rm_rf(target)
|
|
75
|
-
|
|
77
|
+
# `target` is the Flutter project root, not a directory that contains
|
|
78
|
+
# the template root. Copying a source directory onto a target that was
|
|
79
|
+
# already created by FileUtils can produce
|
|
80
|
+
# `build/client/ruflet_flutter_template/...`, leaving `build/client`
|
|
81
|
+
# without pubspec.yaml and making Flutter reject it as a project.
|
|
82
|
+
FileUtils.mkdir_p(target)
|
|
83
|
+
Dir.children(template_root).each do |entry|
|
|
84
|
+
copy_client_template_entry(
|
|
85
|
+
template_root, target, entry)
|
|
86
|
+
end
|
|
76
87
|
prune_client_template(target)
|
|
77
88
|
write_client_template_revision(target, installed_template_revision(template_root))
|
|
78
89
|
end
|
|
@@ -261,6 +272,43 @@ module Ruflet
|
|
|
261
272
|
end
|
|
262
273
|
end
|
|
263
274
|
|
|
275
|
+
def copy_client_template_entry(template_root, target, relative)
|
|
276
|
+
return if generated_client_template_path?(relative)
|
|
277
|
+
|
|
278
|
+
source = File.join(template_root, relative)
|
|
279
|
+
destination = File.join(target, relative)
|
|
280
|
+
stat = File.lstat(source)
|
|
281
|
+
if stat.symlink?
|
|
282
|
+
FileUtils.mkdir_p(File.dirname(destination))
|
|
283
|
+
FileUtils.ln_s(File.readlink(source), destination)
|
|
284
|
+
elsif stat.directory?
|
|
285
|
+
FileUtils.mkdir_p(destination)
|
|
286
|
+
Dir.children(source).each do |entry|
|
|
287
|
+
copy_client_template_entry(
|
|
288
|
+
template_root, target, File.join(relative, entry))
|
|
289
|
+
end
|
|
290
|
+
else
|
|
291
|
+
FileUtils.mkdir_p(File.dirname(destination))
|
|
292
|
+
FileUtils.cp(source, destination, preserve: true)
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def generated_client_template_path?(relative)
|
|
297
|
+
components = relative.split(File::SEPARATOR)
|
|
298
|
+
generated_components = %w[
|
|
299
|
+
.build .cache .claude .dart_tool .git .idea .swiftpm .symlinks
|
|
300
|
+
build DerivedData Pods xcuserdata
|
|
301
|
+
]
|
|
302
|
+
return true if components.any? { |component| generated_components.include?(component) }
|
|
303
|
+
return true if components.any? { |component| component.start_with?(".build-") }
|
|
304
|
+
|
|
305
|
+
generated_files = %w[
|
|
306
|
+
.DS_Store Package.resolved Podfile.lock local.properties
|
|
307
|
+
pubspec_overrides.yaml
|
|
308
|
+
]
|
|
309
|
+
generated_files.include?(components.last) || components.last.end_with?(".xcuserstate")
|
|
310
|
+
end
|
|
311
|
+
|
|
264
312
|
def write_default_ruflet_config(root, app_name)
|
|
265
313
|
File.write(File.join(root, "ruflet.yaml"), <<~YAML)
|
|
266
314
|
app:
|
|
@@ -11,23 +11,23 @@ require "net/http"
|
|
|
11
11
|
require "uri"
|
|
12
12
|
require "thread"
|
|
13
13
|
require "io/console"
|
|
14
|
+
require "open3"
|
|
14
15
|
require "time"
|
|
15
16
|
|
|
16
17
|
module Ruflet
|
|
17
18
|
module CLI
|
|
18
19
|
module RunCommand
|
|
19
|
-
|
|
20
|
+
# Release assets were published as ruflet_client-* before the preview
|
|
21
|
+
# client became Ruflet Explorer. Ask for the current names, but keep
|
|
22
|
+
# recognising the old ones so an existing release still resolves.
|
|
23
|
+
ASSET_PREFIX = "ruflet_explorer"
|
|
24
|
+
LEGACY_ASSET_PREFIX = "ruflet_client"
|
|
25
|
+
CLIENT_CHANNEL_MANIFEST = "#{ASSET_PREFIX}-manifest.json"
|
|
26
|
+
LEGACY_CLIENT_CHANNEL_MANIFEST = "#{LEGACY_ASSET_PREFIX}-manifest.json"
|
|
20
27
|
DEFAULT_CLIENT_UPDATE_INTERVAL = 6 * 60 * 60
|
|
21
28
|
|
|
22
29
|
def command_run(args)
|
|
23
|
-
options =
|
|
24
|
-
parser = OptionParser.new do |o|
|
|
25
|
-
o.on("--web") { options[:target] = "web" }
|
|
26
|
-
o.on("--desktop") { options[:target] = "desktop" }
|
|
27
|
-
o.on("--port PORT", Integer) { |v| options[:requested_port] = v }
|
|
28
|
-
o.on("--no-reload") { options[:reload] = false }
|
|
29
|
-
end
|
|
30
|
-
parser.parse!(args)
|
|
30
|
+
options = parse_run_options(args)
|
|
31
31
|
|
|
32
32
|
script_token = args.shift || "main"
|
|
33
33
|
script_path = resolve_script(script_token)
|
|
@@ -87,7 +87,7 @@ module Ruflet
|
|
|
87
87
|
return status.success? ? 0 : (status.exitstatus || 1) unless run_state[:restart]
|
|
88
88
|
|
|
89
89
|
# Full restart requested ("R"): respawn the backend; connected
|
|
90
|
-
# clients reconnect and re-register on their own (
|
|
90
|
+
# clients reconnect and re-register on their own (Ruflet-style).
|
|
91
91
|
run_state[:restart] = false
|
|
92
92
|
puts "Restarting app..."
|
|
93
93
|
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
@@ -125,6 +125,18 @@ module Ruflet
|
|
|
125
125
|
|
|
126
126
|
private
|
|
127
127
|
|
|
128
|
+
def parse_run_options(args)
|
|
129
|
+
options = { target: "mobile", requested_port: 8550, reload: true }
|
|
130
|
+
parser = OptionParser.new do |o|
|
|
131
|
+
o.on("--web") { options[:target] = "web" }
|
|
132
|
+
o.on("--desktop") { options[:target] = "desktop" }
|
|
133
|
+
o.on("--port PORT", Integer) { |v| options[:requested_port] = v }
|
|
134
|
+
o.on("--no-reload") { options[:reload] = false }
|
|
135
|
+
end
|
|
136
|
+
parser.parse!(args)
|
|
137
|
+
options
|
|
138
|
+
end
|
|
139
|
+
|
|
128
140
|
def build_runtime_command(script_path, gemfile_path:, env:, reload: false)
|
|
129
141
|
entry_script = script_path
|
|
130
142
|
if reload
|
|
@@ -399,8 +411,8 @@ module Ruflet
|
|
|
399
411
|
nil
|
|
400
412
|
end
|
|
401
413
|
|
|
402
|
-
def launch_desktop_client(url)
|
|
403
|
-
cmd = detect_desktop_client_command(url)
|
|
414
|
+
def launch_desktop_client(url, root: nil)
|
|
415
|
+
cmd = detect_desktop_client_command(url, root: root)
|
|
404
416
|
unless cmd
|
|
405
417
|
warn "Desktop client executable not found."
|
|
406
418
|
warn "Set RUFLET_CLIENT_DIR to your client path."
|
|
@@ -456,11 +468,13 @@ module Ruflet
|
|
|
456
468
|
.find { |path| executable_file?(path) }
|
|
457
469
|
end
|
|
458
470
|
|
|
459
|
-
def detect_desktop_client_command(url)
|
|
460
|
-
root
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
471
|
+
def detect_desktop_client_command(url, root: nil)
|
|
472
|
+
unless root
|
|
473
|
+
root = ENV["RUFLET_CLIENT_DIR"]
|
|
474
|
+
root = File.expand_path("ruflet_client", Dir.pwd) if root.to_s.strip.empty?
|
|
475
|
+
root = nil unless Dir.exist?(root)
|
|
476
|
+
root ||= ensure_prebuilt_client(desktop: true)
|
|
477
|
+
end
|
|
464
478
|
return nil unless root && Dir.exist?(root)
|
|
465
479
|
|
|
466
480
|
host_os = RbConfig::CONFIG["host_os"]
|
|
@@ -530,13 +544,15 @@ module Ruflet
|
|
|
530
544
|
FileUtils.mkdir_p(cache_root)
|
|
531
545
|
|
|
532
546
|
wanted_assets = []
|
|
533
|
-
wanted_assets << { kind: :web, name: "
|
|
547
|
+
wanted_assets << { kind: :web, name: "#{ASSET_PREFIX}-web.tar.gz" } if web
|
|
534
548
|
if desktop
|
|
535
549
|
desktop_asset = desktop_asset_name_for(platform)
|
|
536
550
|
return nil if desktop_asset.nil?
|
|
537
551
|
wanted_assets << { kind: :desktop, name: desktop_asset, platform: platform }
|
|
538
552
|
end
|
|
539
|
-
cache_ready = wanted_assets.empty? || prebuilt_assets_present?(
|
|
553
|
+
cache_ready = wanted_assets.empty? || prebuilt_assets_present?(
|
|
554
|
+
cache_root, web: web, desktop: desktop, platform: platform
|
|
555
|
+
)
|
|
540
556
|
release = nil
|
|
541
557
|
if !force && cache_ready
|
|
542
558
|
ensure_client_manifest(cache_root, platform: platform)
|
|
@@ -573,7 +589,10 @@ module Ruflet
|
|
|
573
589
|
puts "Downloading prebuilt client asset: #{resolved_name}"
|
|
574
590
|
archive_path = File.join(tmpdir, resolved_name)
|
|
575
591
|
download_file(asset.fetch("browser_download_url"), archive_path)
|
|
576
|
-
subdir = wanted[:kind]
|
|
592
|
+
subdir = case wanted[:kind]
|
|
593
|
+
when :web then "web"
|
|
594
|
+
when :desktop then "desktop"
|
|
595
|
+
end
|
|
577
596
|
target = File.join(cache_root, subdir)
|
|
578
597
|
FileUtils.rm_rf(target) if force && Dir.exist?(target)
|
|
579
598
|
FileUtils.mkdir_p(target)
|
|
@@ -591,7 +610,9 @@ module Ruflet
|
|
|
591
610
|
end
|
|
592
611
|
end
|
|
593
612
|
|
|
594
|
-
if prebuilt_assets_present?(
|
|
613
|
+
if prebuilt_assets_present?(
|
|
614
|
+
cache_root, web: web, desktop: desktop, platform: platform
|
|
615
|
+
)
|
|
595
616
|
write_client_manifest(cache_root, platform: platform, release: release, assets: installed_assets)
|
|
596
617
|
return cache_root
|
|
597
618
|
end
|
|
@@ -647,9 +668,9 @@ module Ruflet
|
|
|
647
668
|
|
|
648
669
|
def desktop_asset_name_for(platform)
|
|
649
670
|
case platform
|
|
650
|
-
when "macos" then "
|
|
651
|
-
when "linux" then "
|
|
652
|
-
when "windows" then "
|
|
671
|
+
when "macos" then "#{ASSET_PREFIX}-macos-universal.zip"
|
|
672
|
+
when "linux" then "#{ASSET_PREFIX}-linux-x64.tar.gz"
|
|
673
|
+
when "windows" then "#{ASSET_PREFIX}-windows-x64.zip"
|
|
653
674
|
end
|
|
654
675
|
end
|
|
655
676
|
|
|
@@ -659,7 +680,7 @@ module Ruflet
|
|
|
659
680
|
|
|
660
681
|
def fetch_release_for_version(wanted_assets: [])
|
|
661
682
|
releases = []
|
|
662
|
-
channel = client_release_channel
|
|
683
|
+
channel = client_release_channel(wanted_assets: wanted_assets)
|
|
663
684
|
if channel != "stable"
|
|
664
685
|
rolling = release_by_tag(channel)
|
|
665
686
|
releases << rolling if rolling && rolling_release_complete?(rolling)
|
|
@@ -670,13 +691,22 @@ module Ruflet
|
|
|
670
691
|
releases.compact.find { |release| release_has_wanted_assets?(release, wanted_assets) }
|
|
671
692
|
end
|
|
672
693
|
|
|
673
|
-
def client_release_channel
|
|
674
|
-
|
|
675
|
-
value
|
|
694
|
+
def client_release_channel(wanted_assets: [])
|
|
695
|
+
default_channel = "prebuild-main"
|
|
696
|
+
value = ENV.fetch("RUFLET_CLIENT_CHANNEL", default_channel).to_s.strip
|
|
697
|
+
value.empty? ? default_channel : value
|
|
676
698
|
end
|
|
677
699
|
|
|
678
700
|
def rolling_release_complete?(release)
|
|
679
|
-
release
|
|
701
|
+
!channel_manifest_asset(release).nil?
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
# The manifest is written last, so its presence means the run finished
|
|
705
|
+
# uploading every platform. Accept either naming for releases published
|
|
706
|
+
# before the assets were renamed.
|
|
707
|
+
def channel_manifest_asset(release)
|
|
708
|
+
names = [CLIENT_CHANNEL_MANIFEST, LEGACY_CLIENT_CHANNEL_MANIFEST]
|
|
709
|
+
release.fetch("assets", []).find { |asset| names.include?(asset["name"]) }
|
|
680
710
|
end
|
|
681
711
|
|
|
682
712
|
def release_has_wanted_assets?(release, wanted_assets)
|
|
@@ -687,8 +717,7 @@ module Ruflet
|
|
|
687
717
|
end
|
|
688
718
|
|
|
689
719
|
def client_release_revision(release)
|
|
690
|
-
|
|
691
|
-
source = marker || release
|
|
720
|
+
source = channel_manifest_asset(release) || release
|
|
692
721
|
[source["id"], source["updated_at"] || source["published_at"], source["size"]].compact.join(":")
|
|
693
722
|
end
|
|
694
723
|
|
|
@@ -750,7 +779,7 @@ module Ruflet
|
|
|
750
779
|
|
|
751
780
|
def release_asset_matches?(name, kind, platform)
|
|
752
781
|
n = name.to_s.downcase
|
|
753
|
-
return false unless n.include?(
|
|
782
|
+
return false unless n.include?(ASSET_PREFIX) || n.include?(LEGACY_ASSET_PREFIX)
|
|
754
783
|
|
|
755
784
|
if kind == :web
|
|
756
785
|
return n.include?("web") && (n.end_with?(".tar.gz") || n.end_with?(".zip"))
|
|
@@ -758,7 +787,10 @@ module Ruflet
|
|
|
758
787
|
|
|
759
788
|
case platform
|
|
760
789
|
when "macos"
|
|
761
|
-
|
|
790
|
+
# Releases cut before the experimental channel was retired still carry
|
|
791
|
+
# ruflet_explorer-macos-experimental-universal.zip. Installing that as
|
|
792
|
+
# the desktop client would hand back the wrong app, so skip it.
|
|
793
|
+
n.include?("macos") && !n.include?("experimental") && n.end_with?(".zip")
|
|
762
794
|
when "linux"
|
|
763
795
|
n.include?("linux") && (n.end_with?(".tar.gz") || n.end_with?(".tgz"))
|
|
764
796
|
when "windows"
|
data/lib/ruflet/cli.rb
CHANGED
|
@@ -69,11 +69,16 @@ module Ruflet
|
|
|
69
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
|
-
ruflet build <apk|android|ios|aab|web|macos|windows|linux> [--self] [--verbose]
|
|
72
|
+
ruflet build <apk|android|ios|ipa|aab|web|macos|windows|linux> [--lite|--full|--self] [--verbose]
|
|
73
73
|
ruflet install [--device DEVICE_ID] [--verbose]
|
|
74
74
|
ruflet devices
|
|
75
75
|
ruflet emulators
|
|
76
76
|
ruflet doctor [--fix] [--verbose]
|
|
77
|
+
|
|
78
|
+
Build profiles:
|
|
79
|
+
--self Self-contained lite build (default profile)
|
|
80
|
+
--self --full Self-contained full CRuby build with locked project gems
|
|
81
|
+
--full Also implies --self
|
|
77
82
|
HELP
|
|
78
83
|
end
|
|
79
84
|
|
data/lib/ruflet/hot_reload.rb
CHANGED
|
@@ -111,7 +111,29 @@ module Ruflet
|
|
|
111
111
|
end
|
|
112
112
|
|
|
113
113
|
def watched_files
|
|
114
|
-
files =
|
|
114
|
+
files = []
|
|
115
|
+
directories = [@watch_root]
|
|
116
|
+
until directories.empty?
|
|
117
|
+
directory = directories.pop
|
|
118
|
+
begin
|
|
119
|
+
Dir.each_child(directory) do |name|
|
|
120
|
+
# Prune before descending: generated Flutter/build trees can
|
|
121
|
+
# contain thousands of files and are never reload inputs.
|
|
122
|
+
next if name.start_with?(".") || EXCLUDED_DIRECTORIES.include?(name)
|
|
123
|
+
|
|
124
|
+
path = File.join(directory, name)
|
|
125
|
+
if File.directory?(path)
|
|
126
|
+
directories << path unless File.symlink?(path)
|
|
127
|
+
elsif name.end_with?(".rb") && File.file?(path)
|
|
128
|
+
files << path
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
rescue Errno::ENOENT, Errno::ENOTDIR, Errno::EACCES
|
|
132
|
+
# Editors/build tools may remove a directory during a snapshot.
|
|
133
|
+
next
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
files.sort!
|
|
115
137
|
files << @script unless files.include?(@script) || !File.file?(@script)
|
|
116
138
|
files
|
|
117
139
|
end
|
|
@@ -158,15 +180,6 @@ module Ruflet
|
|
|
158
180
|
$LOADED_FEATURES.reject! { |feature| watched.key?(feature) }
|
|
159
181
|
end
|
|
160
182
|
|
|
161
|
-
def excluded_path?(path)
|
|
162
|
-
relative = path.delete_prefix("#{@watch_root}#{File::SEPARATOR}")
|
|
163
|
-
return false if relative == path
|
|
164
|
-
|
|
165
|
-
relative.split(File::SEPARATOR).any? do |component|
|
|
166
|
-
component.start_with?(".") || EXCLUDED_DIRECTORIES.include?(component)
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
|
|
170
183
|
def file_snapshot
|
|
171
184
|
snapshot = {}
|
|
172
185
|
watched_files.each do |path|
|
data/lib/ruflet/version.rb
CHANGED