ruflet_cli 0.0.6 → 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 +4 -4
- data/lib/ruflet/cli/build_command.rb +171 -40
- data/lib/ruflet/cli/new_command.rb +145 -1
- data/lib/ruflet/cli/run_command.rb +36 -31
- data/lib/ruflet/cli/templates.rb +3 -3
- data/lib/ruflet/cli.rb +3 -3
- data/lib/ruflet/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bda605a60090590b4fd902f1b14a3b81c44fbe28cdd08ece037611feae2d775c
|
|
4
|
+
data.tar.gz: f52031603e160ccaeefe976e4b3edd24ea383f03d61dca9f15141411d7ee030e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
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
|
-
|
|
101
|
-
|
|
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["
|
|
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["
|
|
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
|
-
|
|
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
|
|
@@ -9,16 +9,18 @@ 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", requested_port: 8550 }
|
|
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("--port PORT", Integer) { |v| options[:requested_port] = v }
|
|
22
24
|
end
|
|
23
25
|
parser.parse!(args)
|
|
24
26
|
|
|
@@ -30,7 +32,7 @@ module Ruflet
|
|
|
30
32
|
return 1
|
|
31
33
|
end
|
|
32
34
|
|
|
33
|
-
selected_port = resolve_backend_port(options[:target])
|
|
35
|
+
selected_port = resolve_backend_port(options[:target], requested_port: options[:requested_port])
|
|
34
36
|
return 1 unless selected_port
|
|
35
37
|
env = {
|
|
36
38
|
"RUFLET_TARGET" => options[:target],
|
|
@@ -40,20 +42,12 @@ module Ruflet
|
|
|
40
42
|
assets_dir = File.join(File.dirname(script_path), "assets")
|
|
41
43
|
env["RUFLET_ASSETS_DIR"] = assets_dir if File.directory?(assets_dir)
|
|
42
44
|
|
|
43
|
-
print_run_banner(target: options[:target], port: selected_port)
|
|
45
|
+
print_run_banner(target: options[:target], requested_port: options[:requested_port], port: selected_port)
|
|
44
46
|
print_mobile_qr_hint(port: selected_port) if options[:target] == "mobile"
|
|
45
47
|
|
|
46
48
|
gemfile_path = find_nearest_gemfile(Dir.pwd)
|
|
47
|
-
cmd =
|
|
48
|
-
|
|
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
|
|
49
|
+
cmd = build_runtime_command(script_path, gemfile_path: gemfile_path, env: env)
|
|
50
|
+
return 1 unless cmd
|
|
57
51
|
|
|
58
52
|
child_pid = Process.spawn(env, *cmd, pgroup: true)
|
|
59
53
|
launched_client_pids = launch_target_client(options[:target], selected_port)
|
|
@@ -93,10 +87,23 @@ module Ruflet
|
|
|
93
87
|
end
|
|
94
88
|
end
|
|
95
89
|
end
|
|
90
|
+
|
|
96
91
|
end
|
|
97
92
|
|
|
98
93
|
private
|
|
99
94
|
|
|
95
|
+
def build_runtime_command(script_path, gemfile_path:, env:)
|
|
96
|
+
if gemfile_path
|
|
97
|
+
env["BUNDLE_GEMFILE"] = gemfile_path
|
|
98
|
+
bundle_ready = system(env, RbConfig.ruby, "-S", "bundle", "check", out: File::NULL, err: File::NULL)
|
|
99
|
+
return nil unless bundle_ready || system(env, RbConfig.ruby, "-S", "bundle", "install")
|
|
100
|
+
|
|
101
|
+
return [RbConfig.ruby, "-rbundler/setup", script_path]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
[RbConfig.ruby, script_path]
|
|
105
|
+
end
|
|
106
|
+
|
|
100
107
|
def resolve_script(token)
|
|
101
108
|
path = File.expand_path(token, Dir.pwd)
|
|
102
109
|
return path if File.file?(path)
|
|
@@ -120,12 +127,14 @@ module Ruflet
|
|
|
120
127
|
end
|
|
121
128
|
end
|
|
122
129
|
|
|
123
|
-
def print_run_banner(target:, port:)
|
|
124
|
-
if
|
|
125
|
-
puts "Requested 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}"
|
|
126
133
|
end
|
|
127
134
|
if target == "desktop"
|
|
128
135
|
puts "Ruflet desktop URL: http://localhost:#{port}"
|
|
136
|
+
elsif target == "mobile"
|
|
137
|
+
puts "Ruflet target: #{target}"
|
|
129
138
|
else
|
|
130
139
|
puts "Ruflet target: #{target}"
|
|
131
140
|
puts "Ruflet URL: http://localhost:#{port}"
|
|
@@ -156,9 +165,11 @@ module Ruflet
|
|
|
156
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)
|
|
157
166
|
Process.detach(web_pid)
|
|
158
167
|
wait_for_server_boot(web_port)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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}"
|
|
162
173
|
puts "Ruflet backend ws: ws://localhost:#{port}/ws"
|
|
163
174
|
[web_pid, browser_pid].compact
|
|
164
175
|
rescue Errno::ENOENT
|
|
@@ -526,8 +537,6 @@ module Ruflet
|
|
|
526
537
|
puts
|
|
527
538
|
puts "Ruflet mobile connect URL:"
|
|
528
539
|
puts " #{payload}"
|
|
529
|
-
puts "Ruflet server ws URL:"
|
|
530
|
-
puts " ws://0.0.0.0:#{port}/ws"
|
|
531
540
|
puts "Scan this QR from ruflet_client (Connect -> Scan QR):"
|
|
532
541
|
print_ascii_qr(payload)
|
|
533
542
|
puts
|
|
@@ -547,7 +556,7 @@ module Ruflet
|
|
|
547
556
|
end
|
|
548
557
|
probe.close
|
|
549
558
|
return port
|
|
550
|
-
rescue Errno::EADDRINUSE
|
|
559
|
+
rescue Errno::EADDRINUSE, Errno::EACCES, Errno::EPERM
|
|
551
560
|
port += 1
|
|
552
561
|
end
|
|
553
562
|
end
|
|
@@ -555,14 +564,10 @@ module Ruflet
|
|
|
555
564
|
start_port
|
|
556
565
|
end
|
|
557
566
|
|
|
558
|
-
def resolve_backend_port(
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
warn "Port 8550 is required for `ruflet run --#{target}`."
|
|
564
|
-
warn "Stop the process using 8550 and run again."
|
|
565
|
-
nil
|
|
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)
|
|
566
571
|
end
|
|
567
572
|
|
|
568
573
|
def port_available?(port)
|
data/lib/ruflet/cli/templates.rb
CHANGED
|
@@ -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.
|
|
41
|
-
gem "ruflet_server", ">= 0.0.
|
|
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|--
|
|
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
|
data/lib/ruflet/version.rb
CHANGED