ruflet_rails 0.0.11 → 0.0.13
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 +56 -55
- data/lib/generators/ruflet/install/install_generator.rb +46 -15
- data/lib/ruflet/rails/configuration.rb +6 -24
- data/lib/ruflet/rails/generator_hooks.rb +25 -0
- data/lib/ruflet/rails/install_support.rb +40 -67
- data/lib/ruflet/rails/native_app.rb +4 -3
- data/lib/ruflet/rails/protocol/runner.rb +0 -2
- data/lib/ruflet/rails/protocol/web_app.rb +83 -34
- data/lib/ruflet/rails/protocol.rb +0 -4
- data/lib/ruflet/rails/railtie.rb +28 -12
- data/lib/ruflet/rails/resource_component.rb +4 -5
- data/lib/ruflet/rails/web_installer.rb +137 -0
- data/lib/ruflet/rails/webview_app.rb +1 -1
- data/lib/ruflet/rails.rb +16 -201
- data/lib/ruflet/version.rb +1 -1
- data/lib/ruflet_rails.rb +1 -1
- metadata +11 -14
- data/lib/ruflet/rails/protocol/middleware.rb +0 -90
- data/lib/ruflet/rails/protocol/static_index_guard.rb +0 -6
- data/lib/ruflet/rails/protocol/web_app_endpoint.rb +0 -44
- data/lib/ruflet/rails/protocol/web_socket_connection.rb +0 -11
- data/lib/ruflet/rails/protocol/wire_codec.rb +0 -11
- data/lib/ruflet/rails/view.rb +0 -57
data/lib/ruflet/rails.rb
CHANGED
|
@@ -14,26 +14,12 @@ module Ruflet
|
|
|
14
14
|
# Yields the configuration object for block-style setup.
|
|
15
15
|
#
|
|
16
16
|
# Ruflet::Rails.configure do |c|
|
|
17
|
-
# c.
|
|
18
|
-
# c.ws_path = "/ws"
|
|
17
|
+
# c.backend_url = "https://example.com"
|
|
19
18
|
# end
|
|
20
19
|
def configure
|
|
21
20
|
yield config
|
|
22
21
|
end
|
|
23
22
|
|
|
24
|
-
def view_classes
|
|
25
|
-
@view_classes ||= []
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def register_view(view_class)
|
|
29
|
-
view_classes << view_class unless view_classes.include?(view_class)
|
|
30
|
-
view_class
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def render(page, routes: nil, default: nil)
|
|
34
|
-
ViewRouter.new(page, routes: routes, default: default).start
|
|
35
|
-
end
|
|
36
|
-
|
|
37
23
|
# Flet-style routed navigation stack for complex multi-screen apps. Wires
|
|
38
24
|
# up on_route_change / on_view_pop and starts at the current route. See
|
|
39
25
|
# Ruflet::Rails::RouteStack.
|
|
@@ -46,17 +32,6 @@ module Ruflet
|
|
|
46
32
|
RouteStack.new(page, &builder).start
|
|
47
33
|
end
|
|
48
34
|
|
|
49
|
-
def load_views(root)
|
|
50
|
-
return [] if root.to_s.empty?
|
|
51
|
-
|
|
52
|
-
files = Dir[File.join(root.to_s, "components", "**", "*.rb")].sort
|
|
53
|
-
files += Dir[File.join(root.to_s, "**", "*_view.rb")].sort
|
|
54
|
-
|
|
55
|
-
files.each do |file|
|
|
56
|
-
Kernel.load(file)
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
|
|
60
35
|
def sessions
|
|
61
36
|
@sessions ||= SessionRegistry.new
|
|
62
37
|
end
|
|
@@ -66,8 +41,8 @@ module Ruflet
|
|
|
66
41
|
end
|
|
67
42
|
|
|
68
43
|
# WebSocket endpoint for native mobile/desktop clients. The developer
|
|
69
|
-
# declares the entry the same way they declare a web mount —
|
|
70
|
-
# the app shows live in dev code,
|
|
44
|
+
# declares the entry the same way they declare a web mount — the screens
|
|
45
|
+
# the app shows live in dev code, never in framework auto-discovery:
|
|
71
46
|
#
|
|
72
47
|
# # a standalone Ruflet app file (Ruflet.run/MyApp.new.run), per session:
|
|
73
48
|
# match "/ws", to: Ruflet::Rails.endpoint(app_file: Rails.root.join("app/ruflet/main.rb")), via: :all
|
|
@@ -78,64 +53,46 @@ module Ruflet
|
|
|
78
53
|
# # a custom block:
|
|
79
54
|
# match "/ws", to: Ruflet::Rails.endpoint { |page| MyHome.render(page) }, via: :all
|
|
80
55
|
#
|
|
81
|
-
#
|
|
82
|
-
# auto-
|
|
83
|
-
# is a framework fallback for the zero-config case — declare an entry above
|
|
84
|
-
# to own the home screen in your code.
|
|
56
|
+
# One of view:, app_file:, or a block is required — there is no
|
|
57
|
+
# auto-discovery fallback.
|
|
85
58
|
def endpoint(view: nil, app_file: nil, &block)
|
|
86
59
|
sources = [view, app_file, block].compact
|
|
87
60
|
raise ArgumentError, "endpoint accepts only one of view:, app_file:, or a block" if sources.length > 1
|
|
61
|
+
raise ArgumentError, "endpoint requires one of view:, app_file:, or a block" if sources.empty?
|
|
88
62
|
|
|
89
63
|
return Protocol::Runner.new.build_app_endpoint(file_path: app_file) if app_file
|
|
90
64
|
|
|
91
|
-
entry =
|
|
92
|
-
if block
|
|
93
|
-
block
|
|
94
|
-
elsif view
|
|
95
|
-
web_app_entrypoint(view: view)
|
|
96
|
-
else
|
|
97
|
-
->(page) { render(page) }
|
|
98
|
-
end
|
|
65
|
+
entry = block || web_app_entrypoint(view: view)
|
|
99
66
|
Protocol::Runner.new(&entry).build_endpoint
|
|
100
67
|
end
|
|
101
68
|
|
|
102
|
-
#
|
|
69
|
+
# Shorthand for a standalone app-file endpoint.
|
|
103
70
|
#
|
|
104
71
|
# match "/ws", to: Ruflet::Rails.app(Rails.root.join("app/ruflet/main.rb")), via: :all
|
|
105
72
|
def app(file_path)
|
|
106
73
|
endpoint(app_file: file_path)
|
|
107
74
|
end
|
|
108
75
|
|
|
109
|
-
# Backward-compatible alias for older Rails installs.
|
|
110
|
-
def mobile(file_path)
|
|
111
|
-
app(file_path)
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
# Serves the pre-built Flutter web build's index.html at the given route.
|
|
115
|
-
# Static assets (JS, WASM, fonts) are served by ActionDispatch::Static.
|
|
116
|
-
#
|
|
117
|
-
# Usage in routes.rb:
|
|
118
|
-
# get "/showcase", to: Ruflet::Rails.web(build: Rails.root.join("public/showcase"))
|
|
119
|
-
def web(build:)
|
|
120
|
-
Protocol::WebAppEndpoint.new(build_dir: build.to_s)
|
|
121
|
-
end
|
|
122
|
-
|
|
123
76
|
# Self-contained web frontend, mountable under any route. Serves the
|
|
124
77
|
# Flutter web build (with <base href> rewritten to the mount point) and
|
|
125
78
|
# answers the Ruflet WebSocket on the same path. Routes stay routing-only;
|
|
126
|
-
# UI code lives in
|
|
127
|
-
#
|
|
128
|
-
# # all registered RufletView subclasses behind the view router:
|
|
129
|
-
# mount Ruflet::Rails.web_app, at: "/app"
|
|
79
|
+
# UI code lives in dev files:
|
|
130
80
|
#
|
|
131
81
|
# # a single view class (resolved lazily, so reloading works):
|
|
132
82
|
# mount Ruflet::Rails.web_app(view: "CounterView"), at: "/myfrontend"
|
|
133
83
|
#
|
|
134
84
|
# # a standalone Ruflet app file (MyApp.new.run), loaded per session:
|
|
135
85
|
# mount Ruflet::Rails.web_app(app_file: "app/ruflet/showcase/main.rb"), at: "/showcase"
|
|
86
|
+
#
|
|
87
|
+
# # a custom block:
|
|
88
|
+
# mount Ruflet::Rails.web_app { |page| MyHome.render(page) }, at: "/app"
|
|
89
|
+
#
|
|
90
|
+
# One of view:, app_file:, or a block is required — there is no
|
|
91
|
+
# auto-discovery fallback.
|
|
136
92
|
def web_app(view: nil, app_file: nil, build_dir: nil, &app_block)
|
|
137
93
|
sources = [view, app_file, app_block].compact
|
|
138
94
|
raise ArgumentError, "web_app accepts only one of view:, app_file:, or a block" if sources.length > 1
|
|
95
|
+
raise ArgumentError, "web_app requires one of view:, app_file:, or a block" if sources.empty?
|
|
139
96
|
|
|
140
97
|
Protocol::WebApp.new(
|
|
141
98
|
build_dir: build_dir,
|
|
@@ -166,147 +123,5 @@ module Ruflet
|
|
|
166
123
|
view.to_s.constantize
|
|
167
124
|
end
|
|
168
125
|
|
|
169
|
-
# Reads index.html from the web build dir, injects window.__RUFLET_URL__
|
|
170
|
-
# from config.backend_url so the Flutter client knows the WS backend
|
|
171
|
-
# without a rebuild. Falls back to the request base_url if not configured.
|
|
172
|
-
#
|
|
173
|
-
# Usage in a Rails controller:
|
|
174
|
-
# render html: Ruflet::Rails.web_html(request), layout: false
|
|
175
|
-
def web_html(request)
|
|
176
|
-
build_dir = config.web_build_dir.to_s
|
|
177
|
-
html = File.read(File.join(build_dir, "index.html"))
|
|
178
|
-
url = config.backend_url.to_s.strip
|
|
179
|
-
url = request.base_url if url.empty?
|
|
180
|
-
script = "<script>window.__RUFLET_URL__=#{url.to_json};</script>"
|
|
181
|
-
html.include?("</head>") ? html.sub("</head>", "#{script}</head>") : "#{script}#{html}"
|
|
182
|
-
end
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
module Rails
|
|
186
|
-
# ViewRouter dispatches incoming page connections to the correct RufletView
|
|
187
|
-
# subclass based on the current route. It includes SharedControlForwarders
|
|
188
|
-
# so that widget helpers (text, column, safe_area, …) can be called directly
|
|
189
|
-
# inside its own rendering helpers — the same pattern showcase uses.
|
|
190
|
-
class ViewRouter
|
|
191
|
-
include Ruflet::UI::SharedControlForwarders
|
|
192
|
-
|
|
193
|
-
def initialize(page, routes: nil, default: nil)
|
|
194
|
-
@page = page
|
|
195
|
-
@routes = normalize_routes(routes || self.class.discovered_routes)
|
|
196
|
-
@default = default || @routes["/"]
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
def start
|
|
200
|
-
@page.on_route_change = ->(_event) { render }
|
|
201
|
-
render
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
def render
|
|
205
|
-
if root_route_without_default?
|
|
206
|
-
render_route_index
|
|
207
|
-
return
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
target = route_target(@page.route)
|
|
211
|
-
|
|
212
|
-
if target.respond_to?(:render)
|
|
213
|
-
target.render(@page)
|
|
214
|
-
elsif target.respond_to?(:call)
|
|
215
|
-
target.call(@page)
|
|
216
|
-
else
|
|
217
|
-
render_empty_state
|
|
218
|
-
end
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
def self.discovered_routes
|
|
222
|
-
Ruflet::Rails.view_classes.each_with_object({}) do |view_class, routes|
|
|
223
|
-
next unless view_class.respond_to?(:route)
|
|
224
|
-
|
|
225
|
-
routes[view_class.route] = view_class
|
|
226
|
-
end
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
private
|
|
230
|
-
|
|
231
|
-
# Widget builder calls delegate to the global DSL, same as Kernel does,
|
|
232
|
-
# keeping the showcase pattern consistent across all contexts.
|
|
233
|
-
def control_delegate
|
|
234
|
-
Ruflet::DSL
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
def route_target(route)
|
|
238
|
-
path = route_path(route)
|
|
239
|
-
return @routes[path] if @routes.key?(path)
|
|
240
|
-
return @default if path == "/"
|
|
241
|
-
|
|
242
|
-
nil
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
def root_route_without_default?
|
|
246
|
-
route_path(@page.route) == "/" && @default.nil? && @routes["/"].nil? && @routes.any?
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
def normalize_routes(routes)
|
|
250
|
-
routes.to_h.transform_keys { |path| normalize_route(path) }
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
def route_path(route)
|
|
254
|
-
normalize_route(route.to_s.split("?", 2).first)
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
def normalize_route(path)
|
|
258
|
-
value = path.to_s.strip
|
|
259
|
-
return "/" if value.empty? || value == "/"
|
|
260
|
-
|
|
261
|
-
"/#{value.gsub(%r{\A/+|/+\z}, "")}"
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
def render_empty_state
|
|
265
|
-
@page.title = "Ruflet"
|
|
266
|
-
@page.add(
|
|
267
|
-
container(
|
|
268
|
-
expand: true,
|
|
269
|
-
alignment: Ruflet::MainAxisAlignment::CENTER,
|
|
270
|
-
content: text("No Ruflet views found")
|
|
271
|
-
)
|
|
272
|
-
)
|
|
273
|
-
@page.update
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
def render_route_index
|
|
277
|
-
@page.title = "Ruflet"
|
|
278
|
-
@page.add(
|
|
279
|
-
safe_area(
|
|
280
|
-
container(
|
|
281
|
-
expand: true,
|
|
282
|
-
padding: { left: 24, top: 16, right: 24, bottom: 24 },
|
|
283
|
-
content: column(
|
|
284
|
-
spacing: 12,
|
|
285
|
-
controls: [
|
|
286
|
-
text("Ruflet", size: 24, weight: "bold"),
|
|
287
|
-
*route_index_buttons
|
|
288
|
-
]
|
|
289
|
-
)
|
|
290
|
-
),
|
|
291
|
-
expand: true
|
|
292
|
-
)
|
|
293
|
-
)
|
|
294
|
-
@page.update
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
def route_index_buttons
|
|
298
|
-
@routes.keys.sort.map do |path|
|
|
299
|
-
filled_button(
|
|
300
|
-
content: text(route_label(path)),
|
|
301
|
-
on_click: ->(_event) { @page.go(path) }
|
|
302
|
-
)
|
|
303
|
-
end
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
def route_label(path)
|
|
307
|
-
label = path.to_s.delete_prefix("/").tr("_-", " ")
|
|
308
|
-
label.empty? ? "Home" : label.split.map(&:capitalize).join(" ")
|
|
309
|
-
end
|
|
310
|
-
end
|
|
311
126
|
end
|
|
312
127
|
end
|
data/lib/ruflet/version.rb
CHANGED
data/lib/ruflet_rails.rb
CHANGED
|
@@ -11,7 +11,6 @@ require "ruflet_core"
|
|
|
11
11
|
require "ruflet_server"
|
|
12
12
|
require_relative "ruflet/rails/session_registry"
|
|
13
13
|
require_relative "ruflet/rails/form_helpers"
|
|
14
|
-
require_relative "ruflet/rails/view"
|
|
15
14
|
require_relative "ruflet/rails/resource_component"
|
|
16
15
|
require_relative "ruflet/rails/route_stack"
|
|
17
16
|
require_relative "ruflet/rails/webview_app"
|
|
@@ -20,6 +19,7 @@ require_relative "ruflet/rails/view_helpers"
|
|
|
20
19
|
require_relative "ruflet/rails/desktop_launcher"
|
|
21
20
|
require_relative "ruflet/rails/protocol"
|
|
22
21
|
require_relative "ruflet/rails/assets"
|
|
22
|
+
require_relative "ruflet/rails/web_installer"
|
|
23
23
|
require_relative "ruflet/rails/configuration"
|
|
24
24
|
require_relative "ruflet/rails/install_support"
|
|
25
25
|
require_relative "ruflet/rails"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruflet_rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.13
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Adam Moussa Ali
|
|
@@ -29,42 +29,42 @@ dependencies:
|
|
|
29
29
|
requirements:
|
|
30
30
|
- - ">="
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: 0.0.
|
|
32
|
+
version: 0.0.16
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: 0.0.
|
|
39
|
+
version: 0.0.16
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: ruflet_core
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
44
|
- - ">="
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: 0.0.
|
|
46
|
+
version: 0.0.16
|
|
47
47
|
type: :runtime
|
|
48
48
|
prerelease: false
|
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
51
|
- - ">="
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: 0.0.
|
|
53
|
+
version: 0.0.16
|
|
54
54
|
- !ruby/object:Gem::Dependency
|
|
55
55
|
name: ruflet_server
|
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
|
57
57
|
requirements:
|
|
58
58
|
- - ">="
|
|
59
59
|
- !ruby/object:Gem::Version
|
|
60
|
-
version: 0.0.
|
|
60
|
+
version: 0.0.16
|
|
61
61
|
type: :runtime
|
|
62
62
|
prerelease: false
|
|
63
63
|
version_requirements: !ruby/object:Gem::Requirement
|
|
64
64
|
requirements:
|
|
65
65
|
- - ">="
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
|
-
version: 0.0.
|
|
67
|
+
version: 0.0.16
|
|
68
68
|
description: Build cross-platform mobile and desktop apps with Ruby on Rails using
|
|
69
69
|
Ruflet.
|
|
70
70
|
email:
|
|
@@ -82,32 +82,29 @@ files:
|
|
|
82
82
|
- lib/ruflet/rails/configuration.rb
|
|
83
83
|
- lib/ruflet/rails/desktop_launcher.rb
|
|
84
84
|
- lib/ruflet/rails/form_helpers.rb
|
|
85
|
+
- lib/ruflet/rails/generator_hooks.rb
|
|
85
86
|
- lib/ruflet/rails/install_support.rb
|
|
86
87
|
- lib/ruflet/rails/native_app.rb
|
|
87
88
|
- lib/ruflet/rails/protocol.rb
|
|
88
89
|
- lib/ruflet/rails/protocol/context.rb
|
|
89
90
|
- lib/ruflet/rails/protocol/endpoint.rb
|
|
90
91
|
- lib/ruflet/rails/protocol/local_server.rb
|
|
91
|
-
- lib/ruflet/rails/protocol/middleware.rb
|
|
92
92
|
- lib/ruflet/rails/protocol/mobile_loader.rb
|
|
93
93
|
- lib/ruflet/rails/protocol/runner.rb
|
|
94
|
-
- lib/ruflet/rails/protocol/static_index_guard.rb
|
|
95
94
|
- lib/ruflet/rails/protocol/web_app.rb
|
|
96
|
-
- lib/ruflet/rails/protocol/web_app_endpoint.rb
|
|
97
|
-
- lib/ruflet/rails/protocol/web_socket_connection.rb
|
|
98
95
|
- lib/ruflet/rails/protocol/websocket_detection.rb
|
|
99
|
-
- lib/ruflet/rails/protocol/wire_codec.rb
|
|
100
96
|
- lib/ruflet/rails/railtie.rb
|
|
101
97
|
- lib/ruflet/rails/resource_component.rb
|
|
102
98
|
- lib/ruflet/rails/route_stack.rb
|
|
103
99
|
- lib/ruflet/rails/session_registry.rb
|
|
104
|
-
- lib/ruflet/rails/view.rb
|
|
105
100
|
- lib/ruflet/rails/view_helpers.rb
|
|
101
|
+
- lib/ruflet/rails/web_installer.rb
|
|
106
102
|
- lib/ruflet/rails/webview_app.rb
|
|
107
103
|
- lib/ruflet/version.rb
|
|
108
104
|
- lib/ruflet_rails.rb
|
|
109
105
|
homepage: https://github.com/AdamMusa/ruflet/tree/main/packages/ruflet_rails
|
|
110
|
-
licenses:
|
|
106
|
+
licenses:
|
|
107
|
+
- MIT
|
|
111
108
|
metadata: {}
|
|
112
109
|
rdoc_options: []
|
|
113
110
|
require_paths:
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ruflet
|
|
4
|
-
module Rails
|
|
5
|
-
module Protocol
|
|
6
|
-
# Rack middleware inserted before ActionDispatch::Static by the Railtie.
|
|
7
|
-
#
|
|
8
|
-
# Responsibilities:
|
|
9
|
-
# 1. WebSocket at ws_path (/ws) — native mobile/desktop clients.
|
|
10
|
-
# 2. WebSocket at the build dir base path (e.g. /app, /app/) — the
|
|
11
|
-
# Flutter web client uses Uri.base from <base href="/app/"> as its
|
|
12
|
-
# connection URL, so it naturally connects back to /app/ as WebSocket.
|
|
13
|
-
# No URL injection needed — the client finds the server on its own.
|
|
14
|
-
# 3. Block direct HTTP access to the build dir's index so
|
|
15
|
-
# ActionDispatch::Static never leaks it at /app — serves Rails' 404.
|
|
16
|
-
class Middleware
|
|
17
|
-
INDEX_SUFFIXES = ["", "/", "/index.html"].freeze
|
|
18
|
-
|
|
19
|
-
def initialize(app)
|
|
20
|
-
@app = app
|
|
21
|
-
@ws_endpoint = nil
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def call(env)
|
|
25
|
-
cfg = Ruflet::Rails.config
|
|
26
|
-
path = env["PATH_INFO"].to_s
|
|
27
|
-
|
|
28
|
-
if cfg.app_file
|
|
29
|
-
# WS at configured ws_path — native/desktop clients
|
|
30
|
-
if cfg.ws_path && path == normalize(cfg.ws_path) && websocket_upgrade?(env)
|
|
31
|
-
return Context.with_env(env) { ws_endpoint(cfg).call(env) }
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# WS at build dir base path — Flutter web client (Uri.base = /app/)
|
|
35
|
-
if cfg.web_build_dir && build_dir_base?(path, cfg.web_build_dir.to_s) && websocket_upgrade?(env)
|
|
36
|
-
return Context.with_env(env) { ws_endpoint(cfg).call(env) }
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# Block direct HTTP access to the build dir index
|
|
41
|
-
if cfg.web_build_dir && build_dir_index?(path, cfg.web_build_dir.to_s)
|
|
42
|
-
return rails_404
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
Context.with_env(env) { @app.call(env) }
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
private
|
|
49
|
-
|
|
50
|
-
def ws_endpoint(cfg)
|
|
51
|
-
@ws_endpoint ||= Runner.new.build_app_endpoint(file_path: cfg.app_file.to_s)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def websocket_upgrade?(env)
|
|
55
|
-
return false unless env["REQUEST_METHOD"] == "GET"
|
|
56
|
-
|
|
57
|
-
env["HTTP_UPGRADE"].to_s.downcase == "websocket" &&
|
|
58
|
-
env["HTTP_CONNECTION"].to_s.downcase.include?("upgrade") &&
|
|
59
|
-
env["HTTP_SEC_WEBSOCKET_KEY"].to_s != ""
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# Matches /app and /app/ — the Flutter web client connects here.
|
|
63
|
-
def build_dir_base?(path, build_dir)
|
|
64
|
-
base = "/#{File.basename(build_dir)}"
|
|
65
|
-
path == base || path == "#{base}/"
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Matches /app, /app/, /app/index.html — blocks static serving of index.
|
|
69
|
-
def build_dir_index?(path, build_dir)
|
|
70
|
-
base = "/#{File.basename(build_dir)}"
|
|
71
|
-
INDEX_SUFFIXES.any? { |suffix| path == "#{base}#{suffix}" }
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def rails_404
|
|
75
|
-
page = rails_404_page
|
|
76
|
-
[404, { "content-type" => "text/html; charset=utf-8", "content-length" => page.bytesize.to_s }, [page]]
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def rails_404_page
|
|
80
|
-
path = defined?(::Rails) ? ::Rails.public_path.join("404.html") : nil
|
|
81
|
-
path && File.file?(path) ? File.read(path) : "Not Found"
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def normalize(path)
|
|
85
|
-
"/#{path.to_s.gsub(%r{\A/+|/+\z}, "")}"
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
end
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# This file is intentionally empty.
|
|
4
|
-
# StaticIndexGuard was removed — ruflet_rails does not intercept any static
|
|
5
|
-
# paths. The route where the web build is served is defined explicitly in
|
|
6
|
-
# routes.rb, and the backend URL comes from ruflet.yaml at build time.
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ruflet
|
|
4
|
-
module Rails
|
|
5
|
-
module Protocol
|
|
6
|
-
# Rack endpoint that serves the pre-built Flutter web app's index.html.
|
|
7
|
-
#
|
|
8
|
-
# The HTML is read from build_dir and returned as-is — no mutation.
|
|
9
|
-
# The backend URL is baked into the build at compile time via:
|
|
10
|
-
#
|
|
11
|
-
# rake ruflet:build[web] # reads backend_url from ruflet.yaml
|
|
12
|
-
#
|
|
13
|
-
# which passes --dart-define=RUFLET_URL=<backend_url> to flutter build.
|
|
14
|
-
# The Flutter client reads it via String.fromEnvironment('RUFLET_URL').
|
|
15
|
-
#
|
|
16
|
-
# Static assets (JS, WASM, fonts) in the build dir are served by Rails'
|
|
17
|
-
# ActionDispatch::Static as normal.
|
|
18
|
-
#
|
|
19
|
-
# Usage in routes.rb:
|
|
20
|
-
#
|
|
21
|
-
# match "/ws", to: Ruflet::Rails.app(Rails.root.join("app/views/mobile/main.rb")), via: :all
|
|
22
|
-
# get "/showcase", to: Ruflet::Rails.web(build: Rails.root.join("public/showcase"))
|
|
23
|
-
#
|
|
24
|
-
class WebAppEndpoint
|
|
25
|
-
def initialize(build_dir:)
|
|
26
|
-
@index_path = File.join(build_dir.to_s, "index.html")
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def call(_env)
|
|
30
|
-
return not_found unless File.file?(@index_path)
|
|
31
|
-
|
|
32
|
-
html = File.read(@index_path)
|
|
33
|
-
[200, { "content-type" => "text/html; charset=utf-8", "content-length" => html.bytesize.to_s }, [html]]
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
private
|
|
37
|
-
|
|
38
|
-
def not_found
|
|
39
|
-
[404, { "content-type" => "text/plain" }, ["Web build not found"]]
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
data/lib/ruflet/rails/view.rb
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "active_support/core_ext/string/inflections"
|
|
4
|
-
|
|
5
|
-
# RufletView is the base class for all server-driven Ruflet views in a Rails app.
|
|
6
|
-
# It includes Ruflet::UI::SharedControlForwarders so that all widget builder
|
|
7
|
-
# methods (text, column, row, container, safe_area, filled_button, …) are
|
|
8
|
-
# available directly on the view instance — the same way showcase uses them.
|
|
9
|
-
class RufletView
|
|
10
|
-
include Ruflet::UI::SharedControlForwarders
|
|
11
|
-
|
|
12
|
-
attr_reader :page
|
|
13
|
-
|
|
14
|
-
class << self
|
|
15
|
-
def route(path = nil)
|
|
16
|
-
@route_path = normalize_route(path) if path
|
|
17
|
-
@route_path || inferred_route
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def inherited(child)
|
|
21
|
-
super
|
|
22
|
-
Ruflet::Rails.register_view(child) if defined?(Ruflet::Rails) && Ruflet::Rails.respond_to?(:register_view)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
private
|
|
26
|
-
|
|
27
|
-
def inferred_route
|
|
28
|
-
name_part = name.to_s.split("::").last.to_s.sub(/View\z/, "")
|
|
29
|
-
path = name_part.underscore.pluralize
|
|
30
|
-
normalize_route(path.empty? ? "/" : path)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def normalize_route(path)
|
|
34
|
-
value = path.to_s.strip
|
|
35
|
-
return "/" if value.empty? || value == "/"
|
|
36
|
-
|
|
37
|
-
"/#{value.gsub(%r{\A/+|/+\z}, "")}"
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def self.render(page, *args, **kwargs, &block)
|
|
42
|
-
new(page).render(*args, **kwargs, &block)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def initialize(page)
|
|
46
|
-
@page = page
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
private
|
|
50
|
-
|
|
51
|
-
# Delegates all widget builder calls (text, column, row, …) to Ruflet::DSL,
|
|
52
|
-
# matching the same delegate target that Kernel uses. Subclasses can override
|
|
53
|
-
# this to scope widget building to a local WidgetBuilder instance instead.
|
|
54
|
-
def control_delegate
|
|
55
|
-
Ruflet::DSL
|
|
56
|
-
end
|
|
57
|
-
end
|