ruflet_cli 0.0.7 → 0.0.8

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: bd07971461f7a7a4f8f61df41b51b4a6537de8b0dfe152fd71c52148a344c15e
4
- data.tar.gz: 65b0952b26820338f5a4f4115440aac955c0ab09bac94fb3fcc94d19e302eff1
3
+ metadata.gz: bda605a60090590b4fd902f1b14a3b81c44fbe28cdd08ece037611feae2d775c
4
+ data.tar.gz: f52031603e160ccaeefe976e4b3edd24ea383f03d61dca9f15141411d7ee030e
5
5
  SHA512:
6
- metadata.gz: 28b45040c5f0f3b0901d48bedfdd995e644983e3d4e3748696442f21288175722d3b13688f646f4b1cbea662405213b0e6dedf970aa4614b57d9f2000a5e65a8
7
- data.tar.gz: 891b2e7de50d432d0922f7f8aa0e2d28e764d4c8c02def6a7de5c58fbc26c86b9421687516427c84efd0a431e79d546c698962d8d421a54ddf97cb2f246b99ed
6
+ metadata.gz: 87e2ffbe8ff812a0a56c4b7d3a55f8c981d4b614efecd451709bf026687b4c60fd42778e11d5e978c4a34f8bccafb7f93b522ea241a6d6eb51aff3f20e1e9a79
7
+ data.tar.gz: fba28f53a351ba5edb0e1f9bc16b178132ce8f487029d263777f11092cb19df312ea3113301b201e03b39c595ef9ca7a6d87f1e539a9fc619a1822d62969890c
@@ -1,17 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "fileutils"
4
+ require "uri"
4
5
  require "yaml"
5
6
 
6
7
  module Ruflet
7
8
  module CLI
8
9
  module BuildCommand
9
10
  include FlutterSdk
11
+ CLIENT_EXTENSION_MAP = {
12
+ "ads" => { package: "flet_ads", alias: "ruflet_ads" },
13
+ "audio" => { package: "flet_audio", alias: "ruflet_audio" },
14
+ "audio_recorder" => { package: "flet_audio_recorder", alias: "ruflet_audio_recorder" },
15
+ "camera" => { package: "flet_camera", alias: "ruflet_camera" },
16
+ "charts" => { package: "flet_charts", alias: "ruflet_charts" },
17
+ "code_editor" => { package: "flet_code_editor", alias: "ruflet_code_editor" },
18
+ "color_pickers" => { package: "flet_color_pickers", alias: "ruflet_color_picker" },
19
+ "datatable2" => { package: "flet_datatable2", alias: "ruflet_datatable2" },
20
+ "flashlight" => { package: "flet_flashlight", alias: "ruflet_flashlight" },
21
+ "geolocator" => { package: "flet_geolocator", alias: "ruflet_geolocator" },
22
+ "lottie" => { package: "flet_lottie", alias: "ruflet_lottie" },
23
+ "map" => { package: "flet_map", alias: "ruflet_map" },
24
+ "permission_handler" => { package: "flet_permission_handler", alias: "ruflet_permission_handler" },
25
+ "secure_storage" => { package: "flet_secure_storage", alias: "ruflet_secure_storage" },
26
+ "video" => { package: "flet_video", alias: "ruflet_video" },
27
+ "webview" => { package: "flet_webview", alias: "ruflet_webview" }
28
+ }.freeze
10
29
 
11
30
  def command_build(args)
12
31
  platform = (args.shift || "").downcase
13
32
  if platform.empty?
14
- warn "Usage: ruflet build <apk|ios|aab|web|macos|windows|linux>"
33
+ warn "Usage: ruflet build <apk|android|ios|aab|web|macos|windows|linux>"
15
34
  return 1
16
35
  end
17
36
 
@@ -28,11 +47,18 @@ module Ruflet
28
47
  return 1
29
48
  end
30
49
 
50
+ config = load_ruflet_config
31
51
  tools = ensure_flutter!("build", client_dir: client_dir)
32
- ok = prepare_flutter_client(client_dir, tools: tools)
52
+ ok = prepare_flutter_client(client_dir, tools: tools, config: config)
33
53
  return 1 unless ok
34
54
 
35
- ok = system(tools[:env], tools[:flutter], *flutter_cmd, *args, chdir: client_dir)
55
+ build_args = [*flutter_cmd, *args]
56
+ client_url = configured_client_url(config)
57
+ if client_url
58
+ build_args += ["--dart-define", "RUFLET_CLIENT_URL=#{client_url}"]
59
+ end
60
+
61
+ ok = system(tools[:env], tools[:flutter], *build_args, chdir: client_dir)
36
62
  ok ? 0 : 1
37
63
  end
38
64
 
@@ -51,35 +77,72 @@ module Ruflet
51
77
  nil
52
78
  end
53
79
 
54
- def prepare_flutter_client(client_dir, tools:)
55
- apply_build_config(client_dir)
80
+ def prepare_flutter_client(client_dir, tools:, config:)
81
+ apply_service_extension_config(client_dir, config)
82
+ asset_flags = apply_build_config(client_dir, config)
83
+ if asset_flags[:error]
84
+ warn asset_flags[:error]
85
+ return false
86
+ end
56
87
  unless system(tools[:env], tools[:flutter], "pub", "get", chdir: client_dir)
57
88
  warn "flutter pub get failed"
58
89
  return false
59
90
  end
60
91
 
61
- unless system(tools[:env], tools[:dart], "run", "flutter_native_splash:create", chdir: client_dir)
62
- warn "flutter_native_splash failed"
63
- return false
92
+ if asset_flags[:has_splash]
93
+ unless system(tools[:env], tools[:dart], "run", "flutter_native_splash:create", chdir: client_dir)
94
+ warn "flutter_native_splash failed"
95
+ return false
96
+ end
64
97
  end
65
98
 
66
- unless system(tools[:env], tools[:dart], "run", "flutter_launcher_icons", chdir: client_dir)
67
- warn "flutter_launcher_icons failed"
68
- return false
99
+ if asset_flags[:has_icon]
100
+ unless system(tools[:env], tools[:dart], "run", "flutter_launcher_icons", chdir: client_dir)
101
+ warn "flutter_launcher_icons failed"
102
+ return false
103
+ end
69
104
  end
70
105
 
71
106
  true
72
107
  end
73
108
 
74
- def apply_build_config(client_dir)
109
+ def configured_client_url(config)
110
+ candidates = [
111
+ config["ruflet_client_url"],
112
+ (config["app"].is_a?(Hash) ? config["app"]["ruflet_client_url"] : nil)
113
+ ]
114
+ raw = candidates.find { |v| !v.to_s.strip.empty? }
115
+ return nil if raw.nil?
116
+
117
+ value = raw.to_s.strip
118
+ uri = URI.parse(value)
119
+ return nil unless %w[http https ws wss].include?(uri.scheme)
120
+ return nil if uri.host.to_s.strip.empty?
121
+
122
+ value
123
+ rescue URI::InvalidURIError
124
+ nil
125
+ end
126
+
127
+ def load_ruflet_config
75
128
  config_path = ENV["RUFLET_CONFIG"] || "ruflet.yaml"
76
129
  unless File.file?(config_path)
77
130
  alt = "ruflet.yml"
78
131
  config_path = alt if File.file?(alt)
79
132
  end
133
+ return {} unless File.file?(config_path)
80
134
 
135
+ YAML.safe_load(File.read(config_path), aliases: true) || {}
136
+ rescue StandardError => e
137
+ warn "Failed to load ruflet config: #{e.class}: #{e.message}"
138
+ {}
139
+ end
140
+
141
+ def apply_build_config(client_dir, config = {})
142
+ build = config["build"] || {}
143
+ assets = config["assets"] || {}
144
+ config_path = ENV["RUFLET_CONFIG"] || (File.file?("ruflet.yaml") ? "ruflet.yaml" : "ruflet.yml")
81
145
  config_present = File.file?(config_path)
82
- config = config_present ? (YAML.load_file(config_path) || {}) : {}
83
146
  build = config["build"] || {}
84
147
  assets = config["assets"] || {}
85
148
  config_dir = config_present ? File.dirname(File.expand_path(config_path)) : Dir.pwd
@@ -87,44 +150,24 @@ module Ruflet
87
150
  assets_root = build["assets_dir"] || assets["dir"] || config["assets_dir"] || "assets"
88
151
  assets_root = File.expand_path(assets_root, config_dir)
89
152
 
90
- unless config_present || Dir.exist?(assets_root) || ENV["RUFLET_SPLASH"] || ENV["RUFLET_ICON"]
91
- return
92
- end
93
-
94
153
  resolve_asset = lambda do |path|
95
154
  return nil if path.nil? || path.to_s.strip.empty?
96
155
  full = File.expand_path(path.to_s, config_dir)
97
156
  File.file?(full) ? full : nil
98
157
  end
99
158
 
100
- find_first = lambda do |dir, names|
101
- names.each do |name|
102
- candidate = File.join(dir, name)
103
- return candidate if File.file?(candidate)
104
- end
105
- nil
106
- end
159
+ splash_defined = key_defined?(build, "splash_screen") || key_defined?(assets, "splash_screen") || key_defined?(config, "splash_screen")
160
+ icon_defined = key_defined?(build, "icon_launcher") || key_defined?(assets, "icon_launcher") || key_defined?(config, "icon_launcher")
107
161
 
108
- splash = resolve_asset.call(build["splash"] || assets["splash"] || ENV["RUFLET_SPLASH"])
162
+ splash = resolve_asset.call(build["splash_screen"] || assets["splash_screen"] || config["splash_screen"])
109
163
  splash_dark = resolve_asset.call(build["splash_dark"] || build["splash_dark_image"] || assets["splash_dark"])
110
- icon = resolve_asset.call(build["icon"] || assets["icon"] || ENV["RUFLET_ICON"])
164
+ icon = resolve_asset.call(build["icon_launcher"] || assets["icon_launcher"] || config["icon_launcher"])
111
165
  icon_android = resolve_asset.call(build["icon_android"] || assets["icon_android"])
112
166
  icon_ios = resolve_asset.call(build["icon_ios"] || assets["icon_ios"])
113
167
  icon_web = resolve_asset.call(build["icon_web"] || assets["icon_web"])
114
168
  icon_windows = resolve_asset.call(build["icon_windows"] || assets["icon_windows"])
115
169
  icon_macos = resolve_asset.call(build["icon_macos"] || assets["icon_macos"])
116
170
 
117
- if Dir.exist?(assets_root)
118
- splash ||= find_first.call(assets_root, ["splash.png", "splash.jpg", "splash.webp", "splash.bmp"])
119
- splash_dark ||= find_first.call(assets_root, ["splash_dark.png", "splash_dark.jpg", "splash_dark.webp", "splash_dark.bmp"])
120
- icon ||= find_first.call(assets_root, ["icon.png", "icon.jpg", "icon.webp", "icon.bmp"])
121
- icon_android ||= find_first.call(assets_root, ["icon_android.png", "icon_android.jpg", "icon_android.webp"])
122
- icon_ios ||= find_first.call(assets_root, ["icon_ios.png", "icon_ios.jpg", "icon_ios.webp"])
123
- icon_web ||= find_first.call(assets_root, ["icon_web.png", "icon_web.jpg", "icon_web.webp"])
124
- icon_windows ||= find_first.call(assets_root, ["icon_windows.ico", "icon_windows.png"])
125
- icon_macos ||= find_first.call(assets_root, ["icon_macos.png", "icon_macos.jpg", "icon_macos.webp"])
126
- end
127
-
128
171
  splash_color = build["splash_color"]
129
172
  splash_dark_color = build["splash_dark_color"] || build["splash_color_dark"]
130
173
  icon_background = build["icon_background"]
@@ -150,10 +193,19 @@ module Ruflet
150
193
  end
151
194
  copy_asset.call(icon_macos, "icon_macos.png")
152
195
 
196
+ if splash_defined && splash.nil?
197
+ return { has_icon: false, has_splash: false, error: "build config error: splash_screen is set but file was not found" }
198
+ end
199
+ if icon_defined && icon.nil?
200
+ return { has_icon: false, has_splash: false, error: "build config error: icon_launcher is set but file was not found" }
201
+ end
202
+
153
203
  pubspec_path = File.join(client_dir, "pubspec.yaml")
154
- return unless File.file?(pubspec_path)
204
+ unless File.file?(pubspec_path)
205
+ return { has_icon: icon_defined && !icon.nil?, has_splash: splash_defined && !splash.nil?, error: nil }
206
+ end
155
207
 
156
- if icon
208
+ if icon_defined && icon
157
209
  update_pubspec_value(pubspec_path, "flutter_launcher_icons", "image_path", "\"assets/icon.png\"", multiple: true)
158
210
  end
159
211
  update_pubspec_value(pubspec_path, "flutter_launcher_icons", "image_path_android", "\"assets/icon_android.png\"", multiple: true) if icon_android
@@ -168,10 +220,89 @@ module Ruflet
168
220
  update_pubspec_value(pubspec_path, "flutter_launcher_icons", "background_color", "\"#{icon_background}\"") if icon_background
169
221
  update_pubspec_value(pubspec_path, "flutter_launcher_icons", "theme_color", "\"#{theme_color}\"") if theme_color
170
222
 
171
- update_pubspec_value(pubspec_path, "flutter_native_splash", "image", "\"assets/splash.png\"") if splash
223
+ update_pubspec_value(pubspec_path, "flutter_native_splash", "image", "\"assets/splash.png\"") if splash_defined && splash
172
224
  update_pubspec_value(pubspec_path, "flutter_native_splash", "image_dark", "\"assets/splash_dark.png\"") if splash_dark
173
225
  update_pubspec_value(pubspec_path, "flutter_native_splash", "color", "\"#{splash_color}\"") if splash_color
174
226
  update_pubspec_value(pubspec_path, "flutter_native_splash", "color_dark", "\"#{splash_dark_color}\"") if splash_dark_color
227
+
228
+ { has_icon: icon_defined && !icon.nil?, has_splash: splash_defined && !splash.nil?, error: nil }
229
+ end
230
+
231
+ def key_defined?(hash, key)
232
+ hash.is_a?(Hash) && (hash.key?(key) || hash.key?(key.to_sym))
233
+ end
234
+
235
+ def apply_service_extension_config(client_dir, config = {})
236
+ services = Array(config["services"])
237
+ extension_keys = services.map { |v| normalize_extension_key(v) }.compact.uniq
238
+ extension_packages = extension_keys.filter_map { |key| CLIENT_EXTENSION_MAP[key]&.fetch(:package) }.uniq
239
+ extension_aliases = extension_keys.filter_map { |key| CLIENT_EXTENSION_MAP[key]&.fetch(:alias) }.uniq
240
+
241
+ pubspec_path = File.join(client_dir, "pubspec.yaml")
242
+ main_path = File.join(client_dir, "lib", "main.dart")
243
+ prune_client_pubspec(pubspec_path, extension_packages) if File.file?(pubspec_path)
244
+ prune_client_main(main_path, extension_aliases) if File.file?(main_path)
245
+ end
246
+
247
+ def normalize_extension_key(value)
248
+ key = value.to_s.strip.downcase
249
+ return nil if key.empty?
250
+
251
+ key.tr!("-", "_")
252
+ key.gsub!(/\A(flet_)+/, "")
253
+ key.gsub!(/\Aservice_/, "")
254
+ key
255
+ end
256
+
257
+ def prune_client_pubspec(path, selected_packages)
258
+ data = YAML.safe_load(File.read(path), aliases: true) || {}
259
+ deps = (data["dependencies"] || {}).dup
260
+
261
+ deps.keys.each do |name|
262
+ next unless name.start_with?("flet_")
263
+ next if name == "flet"
264
+ next if selected_packages.include?(name)
265
+
266
+ deps.delete(name)
267
+ end
268
+
269
+ data["dependencies"] = deps
270
+ File.write(path, YAML.dump(data))
271
+ end
272
+
273
+ def prune_client_main(path, selected_aliases)
274
+ lines = File.readlines(path)
275
+ alias_to_package = {}
276
+
277
+ lines.each do |line|
278
+ match = line.match(%r{\Aimport 'package:(flet_[^/]+)/\1\.dart' as ([a-zA-Z0-9_]+);})
279
+ next unless match
280
+
281
+ alias_to_package[match[2]] = match[1]
282
+ end
283
+
284
+ kept = lines.select do |line|
285
+ import_match = line.match(%r{\Aimport 'package:(flet_[^/]+)/\1\.dart' as ([a-zA-Z0-9_]+);})
286
+ if import_match
287
+ package_name = import_match[1]
288
+ next true if package_name == "flet"
289
+ next true if selected_aliases.include?(import_match[2])
290
+ next false
291
+ end
292
+
293
+ extension_match = line.match(/\A\s*([a-zA-Z0-9_]+)\.Extension\(\),\s*\z/)
294
+ if extension_match
295
+ extension_alias = extension_match[1]
296
+ package_name = alias_to_package[extension_alias]
297
+ next true if package_name.nil?
298
+ next true if selected_aliases.include?(extension_alias)
299
+ next false
300
+ end
301
+
302
+ true
303
+ end
304
+
305
+ File.write(path, kept.join)
175
306
  end
176
307
 
177
308
  def update_pubspec_value(path, block, key, value, multiple: false)
@@ -1,10 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "fileutils"
4
+ require "yaml"
4
5
 
5
6
  module Ruflet
6
7
  module CLI
7
8
  module NewCommand
9
+ CLIENT_EXTENSION_MAP = {
10
+ "ads" => { package: "flet_ads", alias: "ruflet_ads" },
11
+ "audio" => { package: "flet_audio", alias: "ruflet_audio" },
12
+ "audio_recorder" => { package: "flet_audio_recorder", alias: "ruflet_audio_recorder" },
13
+ "camera" => { package: "flet_camera", alias: "ruflet_camera" },
14
+ "charts" => { package: "flet_charts", alias: "ruflet_charts" },
15
+ "code_editor" => { package: "flet_code_editor", alias: "ruflet_code_editor" },
16
+ "color_pickers" => { package: "flet_color_pickers", alias: "ruflet_color_picker" },
17
+ "datatable2" => { package: "flet_datatable2", alias: "ruflet_datatable2" },
18
+ "flashlight" => { package: "flet_flashlight", alias: "ruflet_flashlight" },
19
+ "geolocator" => { package: "flet_geolocator", alias: "ruflet_geolocator" },
20
+ "lottie" => { package: "flet_lottie", alias: "ruflet_lottie" },
21
+ "map" => { package: "flet_map", alias: "ruflet_map" },
22
+ "permission_handler" => { package: "flet_permission_handler", alias: "ruflet_permission_handler" },
23
+ "secure_storage" => { package: "flet_secure_storage", alias: "ruflet_secure_storage" },
24
+ "video" => { package: "flet_video", alias: "ruflet_video" },
25
+ "webview" => { package: "flet_webview", alias: "ruflet_webview" }
26
+ }.freeze
27
+
8
28
  def command_new(args)
9
29
  app_name = args.shift
10
30
  if app_name.nil? || app_name.strip.empty?
@@ -22,10 +42,11 @@ module Ruflet
22
42
  File.write(File.join(root, "main.rb"), format(Ruflet::CLI::MAIN_TEMPLATE, app_title: humanize_name(File.basename(root))))
23
43
  File.write(File.join(root, "Gemfile"), Ruflet::CLI::GEMFILE_TEMPLATE)
24
44
  File.write(File.join(root, "README.md"), format(Ruflet::CLI::README_TEMPLATE, app_name: File.basename(root)))
45
+ write_default_ruflet_config(root, File.basename(root))
25
46
  copy_ruflet_client_template(root)
47
+ configure_ruflet_client(root)
26
48
 
27
49
  project_name = File.basename(root)
28
- puts "Ruflet app created: #{project_name}"
29
50
  puts "Run:"
30
51
  puts " cd #{project_name}"
31
52
  puts " bundle install"
@@ -69,6 +90,129 @@ module Ruflet
69
90
  end
70
91
  end
71
92
 
93
+ def write_default_ruflet_config(root, app_name)
94
+ File.write(File.join(root, "ruflet.yaml"), <<~YAML)
95
+ app:
96
+ name: #{app_name}
97
+ # Optional production client endpoint used by `ruflet build`.
98
+ # Example: https://api.example.com
99
+ ruflet_client_url: ""
100
+
101
+ # Source of truth for Flutter client extensions/plugins.
102
+ # Examples: camera, video, audio, flashlight, webview, map
103
+ services: []
104
+
105
+ # Build assets configuration consumed by `ruflet build`.
106
+ # Paths are relative to this file unless absolute.
107
+ assets:
108
+ dir: assets
109
+ splash_screen: assets/splash.png
110
+ icon_launcher: assets/icon.png
111
+
112
+ build:
113
+ splash_color: "#FFFFFF"
114
+ splash_dark_color: "#0B0B0B"
115
+ icon_background: "#FFFFFF"
116
+ theme_color: "#FFFFFF"
117
+ YAML
118
+ end
119
+
120
+ def configure_ruflet_client(root)
121
+ config_path = File.join(root, "ruflet.yaml")
122
+ return unless File.file?(config_path)
123
+
124
+ config = YAML.safe_load(File.read(config_path), aliases: true) || {}
125
+ extension_keys = extract_extension_keys(config)
126
+ extension_packages = extension_keys.filter_map { |key| CLIENT_EXTENSION_MAP[key]&.fetch(:package) }.uniq
127
+ extension_aliases = extension_keys.filter_map { |key| CLIENT_EXTENSION_MAP[key]&.fetch(:alias) }.uniq
128
+
129
+ client_dir = File.join(root, "ruflet_client")
130
+ apply_client_manifest!(client_dir, extension_packages, extension_aliases)
131
+ rescue StandardError => e
132
+ warn "Failed to configure ruflet_client from ruflet.yaml: #{e.class}: #{e.message}"
133
+ end
134
+
135
+ def extract_extension_keys(config)
136
+ from_services = Array(config["services"])
137
+
138
+ from_services
139
+ .map { |v| normalize_extension_key(v) }
140
+ .compact
141
+ .uniq
142
+ end
143
+
144
+ def normalize_extension_key(value)
145
+ key = value.to_s.strip.downcase
146
+ return nil if key.empty?
147
+
148
+ key.tr!("-", "_")
149
+ key.gsub!(/\A(flet_)+/, "")
150
+ key.gsub!(/\Aservice_/, "")
151
+ key.gsub!(/\Acontrol_/, "")
152
+ key = "file_picker" if key == "filepicker"
153
+ key
154
+ end
155
+
156
+ def apply_client_manifest!(client_dir, extension_packages, extension_aliases)
157
+ return unless Dir.exist?(client_dir)
158
+
159
+ pubspec_path = File.join(client_dir, "pubspec.yaml")
160
+ main_path = File.join(client_dir, "lib", "main.dart")
161
+ prune_client_pubspec(pubspec_path, extension_packages) if File.file?(pubspec_path)
162
+ prune_client_main(main_path, extension_aliases) if File.file?(main_path)
163
+ end
164
+
165
+ def prune_client_pubspec(path, selected_packages)
166
+ data = YAML.safe_load(File.read(path), aliases: true) || {}
167
+ deps = (data["dependencies"] || {}).dup
168
+
169
+ deps.keys.each do |name|
170
+ next unless name.start_with?("flet_")
171
+ next if name == "flet"
172
+ next if selected_packages.include?(name)
173
+
174
+ deps.delete(name)
175
+ end
176
+
177
+ data["dependencies"] = deps
178
+ File.write(path, YAML.dump(data))
179
+ end
180
+
181
+ def prune_client_main(path, selected_aliases)
182
+ lines = File.readlines(path)
183
+ alias_to_package = {}
184
+
185
+ lines.each do |line|
186
+ match = line.match(%r{\Aimport 'package:(flet_[^/]+)/\1\.dart' as ([a-zA-Z0-9_]+);})
187
+ next unless match
188
+
189
+ alias_to_package[match[2]] = match[1]
190
+ end
191
+
192
+ kept = lines.select do |line|
193
+ import_match = line.match(%r{\Aimport 'package:(flet_[^/]+)/\1\.dart' as ([a-zA-Z0-9_]+);})
194
+ if import_match
195
+ package_name = import_match[1]
196
+ next true if package_name == "flet"
197
+ next true if selected_aliases.include?(import_match[2])
198
+ next false
199
+ end
200
+
201
+ extension_match = line.match(/\A\s*([a-zA-Z0-9_]+)\.Extension\(\),\s*\z/)
202
+ if extension_match
203
+ extension_alias = extension_match[1]
204
+ package_name = alias_to_package[extension_alias]
205
+ next true if package_name.nil? # non-Flet extension lines
206
+ next true if selected_aliases.include?(extension_alias)
207
+ next false
208
+ end
209
+
210
+ true
211
+ end
212
+
213
+ File.write(path, kept.join)
214
+ end
215
+
72
216
  def humanize_name(name)
73
217
  name.to_s.gsub(/[_-]+/, " ").split.map(&:capitalize).join(" ")
74
218
  end
@@ -16,12 +16,11 @@ module Ruflet
16
16
  module CLI
17
17
  module RunCommand
18
18
  def command_run(args)
19
- options = { target: "mobile", hot_reload: true }
19
+ options = { target: "mobile", requested_port: 8550 }
20
20
  parser = OptionParser.new do |o|
21
21
  o.on("--web") { options[:target] = "web" }
22
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 }
23
+ o.on("--port PORT", Integer) { |v| options[:requested_port] = v }
25
24
  end
26
25
  parser.parse!(args)
27
26
 
@@ -33,7 +32,7 @@ module Ruflet
33
32
  return 1
34
33
  end
35
34
 
36
- selected_port = resolve_backend_port(options[:target])
35
+ selected_port = resolve_backend_port(options[:target], requested_port: options[:requested_port])
37
36
  return 1 unless selected_port
38
37
  env = {
39
38
  "RUFLET_TARGET" => options[:target],
@@ -43,11 +42,11 @@ module Ruflet
43
42
  assets_dir = File.join(File.dirname(script_path), "assets")
44
43
  env["RUFLET_ASSETS_DIR"] = assets_dir if File.directory?(assets_dir)
45
44
 
46
- print_run_banner(target: options[:target], port: selected_port)
45
+ print_run_banner(target: options[:target], requested_port: options[:requested_port], port: selected_port)
47
46
  print_mobile_qr_hint(port: selected_port) if options[:target] == "mobile"
48
47
 
49
48
  gemfile_path = find_nearest_gemfile(Dir.pwd)
50
- cmd = build_runtime_command(script_path, gemfile_path: gemfile_path, env: env, hot_reload: options[:hot_reload])
49
+ cmd = build_runtime_command(script_path, gemfile_path: gemfile_path, env: env)
51
50
  return 1 unless cmd
52
51
 
53
52
  child_pid = Process.spawn(env, *cmd, pgroup: true)
@@ -63,22 +62,8 @@ module Ruflet
63
62
  previous_int = Signal.trap("INT") { forward_signal.call("INT") }
64
63
  previous_term = Signal.trap("TERM") { forward_signal.call("TERM") }
65
64
 
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
65
+ _pid, status = Process.wait2(child_pid)
66
+ status.success? ? 0 : (status.exitstatus || 1)
82
67
  ensure
83
68
  Signal.trap("INT", previous_int) if defined?(previous_int) && previous_int
84
69
  Signal.trap("TERM", previous_term) if defined?(previous_term) && previous_term
@@ -107,103 +92,7 @@ module Ruflet
107
92
 
108
93
  private
109
94
 
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:)
95
+ def build_runtime_command(script_path, gemfile_path:, env:)
207
96
  if gemfile_path
208
97
  env["BUNDLE_GEMFILE"] = gemfile_path
209
98
  bundle_ready = system(env, RbConfig.ruby, "-S", "bundle", "check", out: File::NULL, err: File::NULL)
@@ -215,73 +104,6 @@ module Ruflet
215
104
  [RbConfig.ruby, script_path]
216
105
  end
217
106
 
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
-
285
107
  def resolve_script(token)
286
108
  path = File.expand_path(token, Dir.pwd)
287
109
  return path if File.file?(path)
@@ -305,12 +127,14 @@ module Ruflet
305
127
  end
306
128
  end
307
129
 
308
- def print_run_banner(target:, port:)
309
- if target == "mobile" && port != 8550
310
- puts "Requested port 8550 is busy; bound to #{port}"
130
+ def print_run_banner(target:, requested_port:, port:)
131
+ if port != requested_port.to_i
132
+ puts "Requested port #{requested_port} is busy; bound to #{port}"
311
133
  end
312
134
  if target == "desktop"
313
135
  puts "Ruflet desktop URL: http://localhost:#{port}"
136
+ elsif target == "mobile"
137
+ puts "Ruflet target: #{target}"
314
138
  else
315
139
  puts "Ruflet target: #{target}"
316
140
  puts "Ruflet URL: http://localhost:#{port}"
@@ -341,9 +165,11 @@ module Ruflet
341
165
  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)
342
166
  Process.detach(web_pid)
343
167
  wait_for_server_boot(web_port)
344
- browser_pid = open_in_browser_app_mode("http://localhost:#{web_port}")
345
- open_in_browser("http://localhost:#{web_port}") if browser_pid.nil?
346
- puts "Ruflet web client: http://localhost:#{web_port}"
168
+ backend_url = "http://localhost:#{port}"
169
+ web_url = "http://localhost:#{web_port}/?#{URI.encode_www_form(url: backend_url)}"
170
+ browser_pid = open_in_browser_app_mode(web_url)
171
+ open_in_browser(web_url) if browser_pid.nil?
172
+ puts "Ruflet web client: #{web_url}"
347
173
  puts "Ruflet backend ws: ws://localhost:#{port}/ws"
348
174
  [web_pid, browser_pid].compact
349
175
  rescue Errno::ENOENT
@@ -711,8 +537,6 @@ module Ruflet
711
537
  puts
712
538
  puts "Ruflet mobile connect URL:"
713
539
  puts " #{payload}"
714
- puts "Ruflet server ws URL:"
715
- puts " ws://0.0.0.0:#{port}/ws"
716
540
  puts "Scan this QR from ruflet_client (Connect -> Scan QR):"
717
541
  print_ascii_qr(payload)
718
542
  puts
@@ -740,8 +564,10 @@ module Ruflet
740
564
  start_port
741
565
  end
742
566
 
743
- def resolve_backend_port(target)
744
- find_available_port(8550)
567
+ def resolve_backend_port(_target, requested_port: 8550)
568
+ base = requested_port.to_i
569
+ base = 8550 if base <= 0
570
+ find_available_port(base)
745
571
  end
746
572
 
747
573
  def port_available?(port)
@@ -8,7 +8,7 @@ module Ruflet
8
8
  page.title = "Counter Demo"
9
9
  count = 0
10
10
  count_text = nil
11
- count_text ||= text(value: count.to_s, size: 40)
11
+ count_text ||= text(value: count.to_s, style: { size: 40 })
12
12
  page.add(
13
13
  container(
14
14
  expand: true,
@@ -37,8 +37,8 @@ module Ruflet
37
37
  GEMFILE_TEMPLATE = <<~GEMFILE
38
38
  source "https://rubygems.org"
39
39
 
40
- gem "ruflet", ">= 0.0.7"
41
- gem "ruflet_server", ">= 0.0.7"
40
+ gem "ruflet", ">= 0.0.8"
41
+ gem "ruflet_server", ">= 0.0.8"
42
42
  GEMFILE
43
43
 
44
44
  README_TEMPLATE = <<~MD
data/lib/ruflet/cli.rb CHANGED
@@ -4,8 +4,8 @@ require "optparse"
4
4
 
5
5
  require_relative "cli/templates"
6
6
  require_relative "cli/new_command"
7
- require_relative "cli/run_command"
8
7
  require_relative "cli/flutter_sdk"
8
+ require_relative "cli/run_command"
9
9
  require_relative "cli/build_command"
10
10
  require_relative "cli/extra_command"
11
11
 
@@ -54,9 +54,9 @@ module Ruflet
54
54
  Commands:
55
55
  ruflet create <appname>
56
56
  ruflet new <appname>
57
- ruflet run [scriptname|path] [--web|--desktop] [--no-hot-reload]
57
+ ruflet run [scriptname|path] [--web|--desktop] [--port PORT]
58
58
  ruflet debug [scriptname|path]
59
- ruflet build <apk|ios|aab|web|macos|windows|linux>
59
+ ruflet build <apk|android|ios|aab|web|macos|windows|linux>
60
60
  ruflet devices
61
61
  ruflet emulators
62
62
  ruflet doctor
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ruflet
4
- VERSION = "0.0.7" unless const_defined?(:VERSION)
4
+ VERSION = "0.0.8" 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.7
4
+ version: 0.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - AdamMusa