ruflet_rails 0.0.10 → 0.0.11

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: 2ffd81f0143fcba01b93a6d0f773d1d0e4aa983a01c4bf282ed383a631273b13
4
- data.tar.gz: 8840d1be44c36b8d82789f9a9d8055bd2ab0f8e893bafe1fc2ab6f8f082f90d0
3
+ metadata.gz: ebf6ba0607ab65bf23420e9d03d39283aac94ad1ffd92bcd6e96c21fa7d79c9a
4
+ data.tar.gz: 74bba5b6bbf7976457a49556f5ee3c81c42f7afe30ede8979e5cf6f3f4e77354
5
5
  SHA512:
6
- metadata.gz: 456e8aaf3966f272a7b5b0945163a8f96955c240577fb5a1f4f5023ec635260eb531872505e03d552199f6d52c5a68839cbb3a2a4115d82d25f3e1c0bcf95765
7
- data.tar.gz: a63099e903e698ceb130f8b678deaa8a2cb238335266615704fdac47b41f1775fc8e1d677789bab3c6a3961034173d323719325ac159d1b679b4dfd772636a03
6
+ metadata.gz: d1027674f71e87d89e24e2ca90bb9906fe60a67b7f23d93b30e90e053e7a710439fe8d22774976f9c83cdbf83ca82b28aa6610eeec6178d413430fa105ced16c
7
+ data.tar.gz: e55b5f9c8a3549fb37833f5cc88d9950cf35428a2bd688884205b6c83a1ff7d68c627a25469a5893d0b81dd445d5c697fe66b80dcac91c57ae129748da9f1e6b
@@ -25,15 +25,11 @@ module Ruflet
25
25
  create_file target, Ruflet::Rails::InstallSupport.default_ruflet_yaml(app_name: app_name)
26
26
  end
27
27
 
28
- def add_routes
29
- target = File.join(destination_root, "config/routes.rb")
30
- return unless File.file?(target)
31
-
32
- route = Ruflet::Rails::InstallSupport.route_snippet(entrypoint: entrypoint_path)
33
- source = File.read(target)
34
- return if source.include?(route)
28
+ def create_ruflet_initializer
29
+ target = File.join(destination_root, Ruflet::Rails::InstallSupport.initializer_path)
30
+ return if File.exist?(target)
35
31
 
36
- insert_into_file target, " #{route}\n", after: /Rails\.application\.routes\.draw do\s*\n/
32
+ create_file target, Ruflet::Rails::InstallSupport.initializer_template(entrypoint: entrypoint_path)
37
33
  end
38
34
 
39
35
  def add_desktop_flag_to_binstubs
@@ -10,17 +10,7 @@ module Ruflet
10
10
  argument :model_name, type: :string
11
11
  argument :attributes, type: :array, default: [], banner: "field:type field:type"
12
12
 
13
- desc "Generate a Rails-first Ruflet resource view for an existing model."
14
-
15
- def create_ruflet_resource_view
16
- create_file(
17
- File.join(destination_root, scaffold_view_path),
18
- Ruflet::Rails::InstallSupport.scaffold_view_template(
19
- model_name: model_name,
20
- attributes: scaffold_attributes
21
- )
22
- )
23
- end
13
+ desc "Generate a Rails-first Ruflet resource component for an existing model."
24
14
 
25
15
  def create_ruflet_resource_component
26
16
  create_file(
@@ -33,15 +23,19 @@ module Ruflet
33
23
  end
34
24
 
35
25
  def print_scaffold_status
36
- say "Ruflet scaffold generated at #{scaffold_view_path}"
37
- say "Ruflet UI component generated at #{scaffold_component_path}"
38
- say "The generated view owns the resource logic; the generated component owns the UI."
26
+ say "Ruflet resource component generated at #{scaffold_component_path}"
27
+ say "Mount it in config/routes.rb:"
28
+ say " mount Ruflet::Rails.web_app(view: #{scaffold_component_class.inspect}), at: \"/#{scaffold_route_segment}\""
39
29
  end
40
30
 
41
31
  private
42
32
 
43
- def scaffold_view_path
44
- Ruflet::Rails::InstallSupport.scaffold_view_path(model_name)
33
+ def scaffold_component_class
34
+ "#{model_name.to_s.camelize}Component"
35
+ end
36
+
37
+ def scaffold_route_segment
38
+ model_name.to_s.underscore.pluralize
45
39
  end
46
40
 
47
41
  def scaffold_component_path
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruflet
4
+ module Rails
5
+ # Resolve Rails assets to absolute URLs the Flutter client can load over
6
+ # HTTP, so server-driven UI can show app images:
7
+ #
8
+ # image(src: Ruflet::Rails.asset_url("logo.png"))
9
+ # image(src: Ruflet::Rails.image_url("brand/header.png"), fit: "cover")
10
+ #
11
+ # The *path* comes from the Rails asset pipeline — digested in production
12
+ # (/assets/logo-<digest>.png), plain otherwise — so it survives
13
+ # fingerprinting and CDNs. The *host* is resolved, in order, from:
14
+ #
15
+ # 1. an explicit `host:` argument
16
+ # 2. Ruflet::Rails.config.backend_url
17
+ # 3. the host the client connected on (the live WebSocket request)
18
+ #
19
+ # The client is a separate device (simulator, phone, browser), so a bare
20
+ # "/assets/..." path would not resolve — the URL must be absolute. If Rails
21
+ # already has an asset_host/CDN configured, the pipeline returns an absolute
22
+ # URL and it is used unchanged. A value that is already a full URL passes
23
+ # through untouched.
24
+ module_function
25
+
26
+ # The base URL the Flutter client uses to reach this Rails app — the single
27
+ # source of truth for asset URLs, the build-time RUFLET_URL define and the
28
+ # desktop launcher. A Rails Ruflet app always needs one, so this always
29
+ # resolves to a usable value:
30
+ #
31
+ # 1. an explicit host: argument
32
+ # 2. Ruflet::Rails.config.backend_url (set it in config/initializers/ruflet.rb)
33
+ # 3. the host the client connected on (the live WebSocket request)
34
+ #
35
+ # Returns "" only when none of those are available (e.g. a build with no
36
+ # configured backend_url) — set config.backend_url to cover that case.
37
+ def backend_url(host: nil)
38
+ candidate = host || config.backend_url
39
+ candidate = request_base_url if candidate.to_s.strip.empty?
40
+ candidate.to_s.strip.sub(%r{/+\z}, "")
41
+ end
42
+
43
+ def asset_url(source, host: nil)
44
+ raw = source.to_s
45
+ return raw if absolute_url?(raw)
46
+
47
+ path = asset_pipeline_path(raw)
48
+ return path if absolute_url?(path)
49
+
50
+ base = backend_url(host: host)
51
+ base.empty? ? path : "#{base}#{path}"
52
+ end
53
+
54
+ # Readability alias for image sources — identical resolution.
55
+ def image_url(source, host: nil)
56
+ asset_url(source, host: host)
57
+ end
58
+
59
+ def asset_pipeline_path(source)
60
+ ::ActionController::Base.helpers.asset_path(source)
61
+ rescue StandardError
62
+ source.start_with?("/") ? source : "/#{source}"
63
+ end
64
+ private_class_method :asset_pipeline_path
65
+
66
+ # Derive scheme://host from the live WebSocket request env so the URL points
67
+ # back at the exact host the client reached — the one address guaranteed to
68
+ # be reachable from that device.
69
+ def request_base_url
70
+ env = Protocol::Context.current_env
71
+ return nil unless env
72
+
73
+ host = env["HTTP_X_FORWARDED_HOST"] || env["HTTP_HOST"]
74
+ return nil if host.to_s.strip.empty?
75
+
76
+ scheme = (env["HTTP_X_FORWARDED_PROTO"] || env["rack.url_scheme"] || "http").to_s.split(",").first.to_s.strip
77
+ scheme = "http" if scheme.empty?
78
+ "#{scheme}://#{host}"
79
+ end
80
+ private_class_method :request_base_url
81
+
82
+ def absolute_url?(value)
83
+ !(value.to_s =~ %r{\A[a-z][a-z0-9+.-]*://}i).nil?
84
+ end
85
+ private_class_method :absolute_url?
86
+ end
87
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruflet
4
+ module Rails
5
+ # Central configuration for ruflet_rails.
6
+ #
7
+ # Mirrors the full ruflet.yaml schema so a Rails app needs only
8
+ # config/initializers/ruflet.rb — no ruflet.yaml on disk.
9
+ #
10
+ # Set in config/initializers/ruflet.rb:
11
+ #
12
+ # Ruflet::Rails.configure do |config|
13
+ # # Runtime / server
14
+ # config.app_file = Rails.root.join("app/views/ruflet/main.rb")
15
+ # config.ws_path = "/ws"
16
+ # config.backend_url = Rails.env.production? ? "https://example.com" : "http://localhost:3000"
17
+ #
18
+ # # Flutter web build output directory
19
+ # config.web_build_dir = Rails.root.join("public/app")
20
+ #
21
+ # # App metadata (ruflet.yaml → app:)
22
+ # config.app_name = "My App"
23
+ #
24
+ # # Services (ruflet.yaml → services:)
25
+ # config.services = []
26
+ #
27
+ # # Assets (ruflet.yaml → assets:)
28
+ # config.splash_screen = Rails.root.join("app/assets/images/splash.png")
29
+ # config.splash_dark = Rails.root.join("app/assets/images/splash_dark.png")
30
+ # config.icon_launcher = Rails.root.join("app/assets/images/icon.png")
31
+ # config.icon_android = Rails.root.join("app/assets/images/icon_android.png")
32
+ # config.icon_ios = Rails.root.join("app/assets/images/icon_ios.png")
33
+ # config.icon_web = Rails.root.join("app/assets/images/icon_web.png")
34
+ # config.icon_windows = Rails.root.join("app/assets/images/icon_windows.png")
35
+ # config.icon_macos = Rails.root.join("app/assets/images/icon_macos.png")
36
+ #
37
+ # # Build options (ruflet.yaml → build:)
38
+ # config.splash_color = "#FFFFFF"
39
+ # config.splash_dark_color = "#000000"
40
+ # config.icon_background = "#FFFFFF"
41
+ # config.theme_color = "#FFFFFF"
42
+ # end
43
+ class Configuration
44
+ # --- Runtime / server ---
45
+
46
+ # Absolute path to the Ruflet app entry-point.
47
+ attr_accessor :app_file
48
+
49
+ # URL path the WebSocket endpoint listens on. Defaults to "/ws".
50
+ attr_accessor :ws_path
51
+
52
+ # Backend base URL. Used as --dart-define=RUFLET_URL at build time
53
+ # and by the desktop launcher. Replaces ruflet.yaml → app.backend_url.
54
+ attr_accessor :backend_url
55
+
56
+ # Absolute directory containing the Flutter web build (index.html + assets).
57
+ attr_accessor :web_build_dir
58
+
59
+ # --- App metadata (ruflet.yaml → app:) ---
60
+
61
+ attr_accessor :app_name
62
+
63
+ # --- Services (ruflet.yaml → services:) ---
64
+
65
+ attr_accessor :services
66
+
67
+ # --- Assets (ruflet.yaml → assets:) ---
68
+
69
+ attr_accessor :splash_screen
70
+ attr_accessor :splash_dark
71
+ attr_accessor :icon_launcher
72
+ attr_accessor :icon_android
73
+ attr_accessor :icon_ios
74
+ attr_accessor :icon_web
75
+ attr_accessor :icon_windows
76
+ attr_accessor :icon_macos
77
+
78
+ # --- Build options (ruflet.yaml → build:) ---
79
+
80
+ attr_accessor :splash_color
81
+ attr_accessor :splash_dark_color
82
+ attr_accessor :icon_background
83
+ attr_accessor :theme_color
84
+
85
+ def initialize
86
+ @ws_path = "/ws"
87
+ @app_file = nil
88
+ @backend_url = nil
89
+ @web_build_dir = nil
90
+ @app_name = nil
91
+ @services = []
92
+ end
93
+
94
+ # Serialises config to the ruflet.yaml hash structure so the CLI can
95
+ # consume it without a yaml file on disk (written to a temp file by
96
+ # the Railtie's build task).
97
+ def to_ruflet_yaml_hash
98
+ hash = {}
99
+
100
+ app = {}
101
+ app["name"] = @app_name if @app_name
102
+ app["backend_url"] = @backend_url if @backend_url
103
+ hash["app"] = app unless app.empty?
104
+
105
+ hash["services"] = Array(@services)
106
+
107
+ assets = {}
108
+ assets["splash_screen"] = @splash_screen.to_s if @splash_screen
109
+ assets["splash_dark"] = @splash_dark.to_s if @splash_dark
110
+ assets["icon_launcher"] = @icon_launcher.to_s if @icon_launcher
111
+ assets["icon_android"] = @icon_android.to_s if @icon_android
112
+ assets["icon_ios"] = @icon_ios.to_s if @icon_ios
113
+ assets["icon_web"] = @icon_web.to_s if @icon_web
114
+ assets["icon_windows"] = @icon_windows.to_s if @icon_windows
115
+ assets["icon_macos"] = @icon_macos.to_s if @icon_macos
116
+ hash["assets"] = assets unless assets.empty?
117
+
118
+ build = {}
119
+ build["splash_color"] = @splash_color if @splash_color
120
+ build["splash_dark_color"] = @splash_dark_color if @splash_dark_color
121
+ build["icon_background"] = @icon_background if @icon_background
122
+ build["theme_color"] = @theme_color if @theme_color
123
+ hash["build"] = build unless build.empty?
124
+
125
+ hash
126
+ end
127
+ end
128
+ end
129
+ end
@@ -113,129 +113,25 @@ module Ruflet
113
113
  File.join("app", "views", "ruflet", "components", names[:plural], "#{names[:singular]}_form.rb")
114
114
  end
115
115
 
116
- def scaffold_view_path(model_name)
117
- names = model_names(model_name)
118
-
119
- File.join("app", "views", "ruflet", "#{names[:plural]}_view.rb")
120
- end
121
-
122
116
  def scaffold_component_path(model_name)
123
117
  names = model_names(model_name)
124
118
 
125
119
  File.join("app", "views", "ruflet", "components", names[:plural], "#{names[:singular]}_component.rb")
126
120
  end
127
121
 
128
- def scaffold_view_template(model_name:, attributes: [])
129
- names = model_names(model_name)
130
- model_class = names[:class_name]
131
- view_class = "#{model_class}View"
132
- component_class = "#{model_class}Component"
133
- title = names[:title]
134
- attrs = normalized_form_attributes(attributes)
135
- resource_fields = scaffold_resource_fields(attrs)
136
- display_fields = scaffold_display_fields(attrs)
137
- display_value_cases = scaffold_display_value_cases(attrs)
138
-
139
- template = <<~RUBY
140
- # frozen_string_literal: true
141
-
142
- require "ruflet_rails"
143
- require_relative "components/#{names[:plural]}/#{names[:singular]}_component"
144
-
145
- class #{view_class} < Ruflet::Rails::ResourceView
146
- route #{("/" + names[:plural]).inspect}
147
-
148
- def render
149
- page.title = resource_title
150
- render_index
151
- end
152
-
153
- private
154
-
155
- def model_class
156
- #{model_class}
157
- end
158
-
159
- def resource_title
160
- #{title.inspect}
161
- end
162
-
163
- def singular_title
164
- model_class.model_name.human.titleize
165
- end
166
-
167
- def records
168
- scope = model_class.respond_to?(:limit) ? model_class.limit(50) : model_class.all
169
- scope.respond_to?(:limit) ? scope.limit(50) : scope.to_a.first(50)
170
- end
171
-
172
- def render_index
173
- page.views = []
174
- page.add(component.render)
175
- end
176
-
177
- def render_show(record)
178
- page.views = []
179
- page.add(component.show(record))
180
- page.update
181
- end
182
-
183
- def component
184
- @component ||= #{component_class}.new(page, controller: self)
185
- end
186
-
187
- def show_record(record)
188
- render_show(record)
189
- end
190
-
191
- def save_record(record, attributes, dialog)
192
- if record.update(attributes)
193
- close_dialog(dialog)
194
- render_index
195
- show_snackbar("\#{singular_title} saved")
196
- else
197
- show_errors(record)
198
- end
199
- end
200
-
201
- def destroy_record(record, dialog)
202
- record.destroy!
203
- close_dialog(dialog)
204
- render_index
205
- show_snackbar("\#{singular_title} deleted")
206
- rescue StandardError => e
207
- show_snackbar(e.message)
208
- end
209
-
210
- def resource_fields
211
- #{resource_fields}
212
- end
213
-
214
- def display_fields
215
- #{display_fields}
216
- end
217
-
218
- def display_value(record, field)
219
- case field
220
- __DISPLAY_VALUE_CASES__
221
- else
222
- record.public_send(field).to_s
223
- end
224
- end
225
-
226
- def primary_label(record)
227
- field = display_fields.first
228
- field ? display_value(record, field) : "##\#{record_id(record)}"
229
- end
230
-
231
- def secondary_label(record)
232
- field = display_fields[1]
233
- field ? display_value(record, field) : nil
234
- end
122
+ # A `case` with no `when` clause is a syntax error, so models without
123
+ # date/time attributes get the plain fallback body instead.
124
+ def scaffold_display_value_body(display_value_cases, indent)
125
+ return "#{indent}record.public_send(field).to_s" if display_value_cases.to_s.strip.empty?
235
126
 
127
+ reindented_cases = display_value_cases.gsub(/^ /, indent)
128
+ <<~RUBY.chomp.gsub(/^/, indent).gsub(/^#{Regexp.escape(indent)}__CASES__$/, reindented_cases)
129
+ case field
130
+ __CASES__
131
+ else
132
+ record.public_send(field).to_s
236
133
  end
237
134
  RUBY
238
- template.gsub(/^[ \t]*__DISPLAY_VALUE_CASES__$/, display_value_cases)
239
135
  end
240
136
 
241
137
  def scaffold_component_template(model_name:, attributes: [])
@@ -253,6 +149,15 @@ module Ruflet
253
149
  require "date"
254
150
  require "ruflet_rails"
255
151
 
152
+ # The model (#{model_class}) is inferred from the class name and the route
153
+ # ("/#{names[:plural]}") is declared in config/routes.rb. This file is YOURS:
154
+ # the whole CRUD UI (index table, detail screen, create/edit form) and the
155
+ # database calls (#{model_class}#update, #destroy!, .new) are explicit below
156
+ # so you can change the UI or logic however you like. The base class only
157
+ # provides reusable helpers: record loading, field inference, dialog
158
+ # open/close, the date/time picker value helpers, and refresh.
159
+ #
160
+ # The same component renders on web and on mobile/desktop.
256
161
  class #{component_class} < Ruflet::Rails::ResourceComponent
257
162
  def render
258
163
  safe_area(
@@ -401,6 +306,7 @@ module Ruflet
401
306
  width: dialog_width,
402
307
  content: column(
403
308
  spacing: 8,
309
+ horizontal_alignment: "stretch",
404
310
  children: [
405
311
  #{control_list}
406
312
  ]
@@ -409,7 +315,14 @@ module Ruflet
409
315
  actions: [
410
316
  text_button(content: text("Cancel"), on_click: ->(_event) { close_dialog(dialog) }),
411
317
  filled_button(content: text("Save"), on_click: ->(_event) {
412
- save_record(record, attributes.call, dialog)
318
+ # Persist with the model (this is your code — change it freely).
319
+ if record.update(attributes.call)
320
+ close_dialog(dialog)
321
+ refresh
322
+ show_snackbar("\#{singular_title} saved")
323
+ else
324
+ show_errors(record)
325
+ end
413
326
  })
414
327
  ],
415
328
  actions_alignment: "end"
@@ -426,7 +339,13 @@ module Ruflet
426
339
  content: text("Permanently remove \#{singular_title} #\#{record_id(record)}?", no_wrap: false),
427
340
  actions: [
428
341
  text_button(content: text("Cancel"), on_click: ->(_event) { close_dialog(dialog) }),
429
- filled_button(content: text("Delete"), on_click: ->(_event) { destroy_record(record, dialog) })
342
+ filled_button(content: text("Delete"), on_click: ->(_event) {
343
+ # Destroy with the model (this is your code — change it freely).
344
+ record.destroy!
345
+ close_dialog(dialog)
346
+ refresh
347
+ show_snackbar("\#{singular_title} deleted")
348
+ })
430
349
  ],
431
350
  actions_alignment: "end"
432
351
  )
@@ -512,7 +431,7 @@ module Ruflet
512
431
  help_text: #{label.inspect},
513
432
  on_change: ->(_event) do
514
433
  close_dialogs(#{control})
515
- page.update(#{display_control}, value: date_display_value(#{control}.props["value"]))
434
+ page.update(#{display_control}, value: date_display_value(#{control}.value))
516
435
  end
517
436
  )
518
437
  #{control}_field = column(
@@ -539,7 +458,7 @@ module Ruflet
539
458
  help_text: #{label.inspect},
540
459
  on_change: ->(_event) do
541
460
  close_dialogs(#{control})
542
- page.update(#{display_control}, value: time_display_value(#{control}.props["value"]))
461
+ page.update(#{display_control}, value: time_display_value(#{control}.value))
543
462
  end
544
463
  )
545
464
  #{control}_field = column(
@@ -569,7 +488,7 @@ module Ruflet
569
488
  close_dialogs(#{control})
570
489
  page.update(
571
490
  #{display_control},
572
- value: date_range_display_value(#{control}.props["start_value"], #{control}.props["end_value"])
491
+ value: date_range_display_value(#{control}.start_value, #{control}.end_value)
573
492
  )
574
493
  end
575
494
  )
@@ -603,13 +522,13 @@ module Ruflet
603
522
  value =
604
523
  case type
605
524
  when "boolean"
606
- "!!#{control}.props[\"value\"]"
525
+ "!!#{control}.value"
607
526
  when "date"
608
- "#{control}.props[\"value\"].to_s.split(\"T\", 2).first"
527
+ "#{control}.value.to_s.split(\"T\", 2).first"
609
528
  when "date_range", "daterange"
610
- "Range.new(Date.parse(#{control}.props[\"start_value\"].to_s), Date.parse(#{control}.props[\"end_value\"].to_s))"
529
+ "Range.new(Date.parse(#{control}.start_value.to_s), Date.parse(#{control}.end_value.to_s))"
611
530
  else
612
- "#{control}.props[\"value\"].to_s"
531
+ "#{control}.value.to_s"
613
532
  end
614
533
 
615
534
  "#{name.inspect} => #{value}"
@@ -648,7 +567,7 @@ module Ruflet
648
567
  spacing: 12,
649
568
  children: [
650
569
  text(title, size: 24, weight: "bold"),
651
- column(spacing: 8, children: ruflet_form_controls(fields)),
570
+ column(spacing: 8, horizontal_alignment: "stretch", children: ruflet_form_controls(fields)),
652
571
  row(
653
572
  spacing: 8,
654
573
  children: [
@@ -827,17 +746,55 @@ module Ruflet
827
746
  value
828
747
  end
829
748
 
830
- def build_args_for_platform(platform)
749
+ def build_args_for_platform(platform, ruflet_url: nil)
831
750
  normalized = normalize_build_platform(platform)
832
751
  return [] if normalized.to_s.empty?
833
752
 
834
- [normalized]
753
+ args = [normalized]
754
+ args += ["--dart-define", "RUFLET_URL=#{ruflet_url}"] if normalized == "web" && ruflet_url.to_s.strip != ""
755
+ args
835
756
  end
836
757
 
837
758
  def default_entrypoint_path
838
759
  File.join("app", "views", "ruflet", "main.rb")
839
760
  end
840
761
 
762
+ def initializer_template(entrypoint: default_entrypoint_path, ws_path: "/ws")
763
+ <<~RUBY
764
+ # frozen_string_literal: true
765
+
766
+ Ruflet::Rails.configure do |config|
767
+ # Ruflet app entry-point. Auto-mounts a WebSocket endpoint at ws_path —
768
+ # no explicit route needed in config/routes.rb.
769
+ config.app_file = Rails.root.join(#{entrypoint.inspect})
770
+
771
+ # URL path the WebSocket endpoint listens on (default: "/ws").
772
+ config.ws_path = #{ws_path.inspect}
773
+
774
+ # Base URL the Flutter client uses to reach this Rails app. Always
775
+ # required: it backs asset URLs (Ruflet::Rails.asset_url), the
776
+ # build-time RUFLET_URL define, and the desktop launcher. At runtime
777
+ # it can fall back to the connecting host, but a build has no request,
778
+ # so set it here. Point it at a LAN IP (not localhost) to test on a
779
+ # real device.
780
+ config.backend_url = ENV.fetch("RUFLET_BACKEND_URL") do
781
+ Rails.env.production? ? "https://example.com" : "http://localhost:3000"
782
+ end
783
+
784
+ # Directory the Flutter web build is served from. Defaults to
785
+ # Rails.root/build/web (where `rake ruflet:build[web]` outputs).
786
+ # Must stay OUTSIDE public/, or Rails would serve it statically and
787
+ # expose the app at a path no route declares.
788
+ # config.web_build_dir = Rails.root.join("build", "web")
789
+ end
790
+ RUBY
791
+ end
792
+
793
+ def initializer_path
794
+ File.join("config", "initializers", "ruflet.rb")
795
+ end
796
+
797
+ # Kept for backward compatibility with apps that use manual mount.
841
798
  def route_snippet(entrypoint: default_entrypoint_path, mount_path: "/ws", helper: "app")
842
799
  %(match "#{mount_path}", to: Ruflet::Rails.#{helper}(Rails.root.join("#{entrypoint}")), via: :all)
843
800
  end