ruflet_rails 0.0.9 → 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 +4 -4
- data/lib/generators/ruflet/install/install_generator.rb +9 -39
- data/lib/generators/ruflet/scaffold/scaffold_generator.rb +10 -16
- data/lib/ruflet/rails/assets.rb +87 -0
- data/lib/ruflet/rails/configuration.rb +129 -0
- data/lib/ruflet/rails/install_support.rb +80 -236
- data/lib/ruflet/rails/native_app.rb +253 -0
- data/lib/ruflet/rails/protocol/endpoint.rb +3 -11
- data/lib/ruflet/rails/protocol/local_server.rb +27 -194
- data/lib/ruflet/rails/protocol/middleware.rb +74 -1
- data/lib/ruflet/rails/protocol/static_index_guard.rb +6 -0
- data/lib/ruflet/rails/protocol/web_app.rb +194 -0
- data/lib/ruflet/rails/protocol/web_app_endpoint.rb +44 -0
- data/lib/ruflet/rails/protocol/websocket_detection.rb +19 -0
- data/lib/ruflet/rails/protocol.rb +3 -0
- data/lib/ruflet/rails/railtie.rb +49 -32
- data/lib/ruflet/rails/resource_component.rb +188 -8
- data/lib/ruflet/rails/route_stack.rb +90 -0
- data/lib/ruflet/rails/view_helpers.rb +58 -0
- data/lib/ruflet/rails/webview_app.rb +54 -0
- data/lib/ruflet/rails.rb +133 -5
- data/lib/ruflet/version.rb +1 -1
- data/lib/ruflet_rails.rb +6 -1
- metadata +33 -10
- data/lib/ruflet/rails/resource_view.rb +0 -124
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ebf6ba0607ab65bf23420e9d03d39283aac94ad1ffd92bcd6e96c21fa7d79c9a
|
|
4
|
+
data.tar.gz: 74bba5b6bbf7976457a49556f5ee3c81c42f7afe30ede8979e5cf6f3f4e77354
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d1027674f71e87d89e24e2ca90bb9906fe60a67b7f23d93b30e90e053e7a710439fe8d22774976f9c83cdbf83ca82b28aa6610eeec6178d413430fa105ced16c
|
|
7
|
+
data.tar.gz: e55b5f9c8a3549fb37833f5cc88d9950cf35428a2bd688884205b6c83a1ff7d68c627a25469a5893d0b81dd445d5c697fe66b80dcac91c57ae129748da9f1e6b
|
|
@@ -6,10 +6,8 @@ require "ruflet/rails/install_support"
|
|
|
6
6
|
module Ruflet
|
|
7
7
|
module Generators
|
|
8
8
|
class InstallGenerator < ::Rails::Generators::Base
|
|
9
|
-
class_option :frontend, type: :boolean, default: false, desc: "Install the Rails-hosted Ruflet web client"
|
|
10
|
-
class_option :web, type: :boolean, default: false, desc: "Install the Rails-hosted Ruflet web client"
|
|
11
9
|
class_option :desktop, type: :boolean, default: false, desc: "Download the server-driven desktop Ruflet client"
|
|
12
|
-
class_option :client, type: :string, default: nil, desc: "Download prebuilt client from GitHub releases:
|
|
10
|
+
class_option :client, type: :string, default: nil, desc: "Download prebuilt client from GitHub releases: desktop or none"
|
|
13
11
|
|
|
14
12
|
desc "Install Ruflet into a Rails app."
|
|
15
13
|
|
|
@@ -27,14 +25,11 @@ module Ruflet
|
|
|
27
25
|
create_file target, Ruflet::Rails::InstallSupport.default_ruflet_yaml(app_name: app_name)
|
|
28
26
|
end
|
|
29
27
|
|
|
30
|
-
def
|
|
31
|
-
target = File.join(destination_root,
|
|
32
|
-
return
|
|
33
|
-
|
|
34
|
-
route = Ruflet::Rails::InstallSupport.route_snippet(entrypoint: entrypoint_path)
|
|
35
|
-
return if File.read(target).include?(route)
|
|
28
|
+
def create_ruflet_initializer
|
|
29
|
+
target = File.join(destination_root, Ruflet::Rails::InstallSupport.initializer_path)
|
|
30
|
+
return if File.exist?(target)
|
|
36
31
|
|
|
37
|
-
|
|
32
|
+
create_file target, Ruflet::Rails::InstallSupport.initializer_template(entrypoint: entrypoint_path)
|
|
38
33
|
end
|
|
39
34
|
|
|
40
35
|
def add_desktop_flag_to_binstubs
|
|
@@ -46,17 +41,8 @@ module Ruflet
|
|
|
46
41
|
|
|
47
42
|
def download_prebuilt_client
|
|
48
43
|
client = requested_client
|
|
49
|
-
@web_client_published = false
|
|
50
44
|
return if client == "none"
|
|
51
45
|
|
|
52
|
-
if %w[web all].include?(client)
|
|
53
|
-
@web_client_published = Ruflet::Rails::InstallSupport.publish_web_build(destination_root)
|
|
54
|
-
if @web_client_published
|
|
55
|
-
say "Ruflet web build copied from build/web to public/#{Ruflet::Rails::InstallSupport.default_web_public_path}"
|
|
56
|
-
return if client == "web"
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
|
|
60
46
|
require "ruflet/cli"
|
|
61
47
|
exit_code = Dir.chdir(destination_root) do
|
|
62
48
|
Ruflet::CLI.command_update([client])
|
|
@@ -64,17 +50,6 @@ module Ruflet
|
|
|
64
50
|
unless exit_code.to_i.zero?
|
|
65
51
|
@client_download_failed = true
|
|
66
52
|
say_status(:warn, "Ruflet client download failed; install files were generated and build/update steps are printed below", :yellow)
|
|
67
|
-
return
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
return unless %w[web all].include?(client)
|
|
71
|
-
|
|
72
|
-
published = Ruflet::Rails::InstallSupport.publish_prebuilt_web_client(destination_root)
|
|
73
|
-
@web_client_published = published
|
|
74
|
-
if published
|
|
75
|
-
say "Ruflet web client published at /#{Ruflet::Rails::InstallSupport.default_web_public_path}/"
|
|
76
|
-
else
|
|
77
|
-
say_status(:warn, "Ruflet web client downloaded, but no prebuilt web index.html was found to publish", :yellow)
|
|
78
53
|
end
|
|
79
54
|
rescue StandardError => e
|
|
80
55
|
@client_download_failed = true
|
|
@@ -85,8 +60,7 @@ module Ruflet
|
|
|
85
60
|
Ruflet::Rails::InstallSupport.install_next_steps(
|
|
86
61
|
target: install_target,
|
|
87
62
|
entrypoint: entrypoint_path,
|
|
88
|
-
client: requested_client
|
|
89
|
-
web_published: !!@web_client_published
|
|
63
|
+
client: requested_client
|
|
90
64
|
).each { |line| say line }
|
|
91
65
|
end
|
|
92
66
|
|
|
@@ -103,22 +77,18 @@ module Ruflet
|
|
|
103
77
|
def requested_client
|
|
104
78
|
explicit = options[:client].to_s.strip.downcase
|
|
105
79
|
unless explicit.empty?
|
|
106
|
-
raise Thor::Error, "--client must be
|
|
80
|
+
raise Thor::Error, "--client must be desktop or none" unless %w[desktop none].include?(explicit)
|
|
107
81
|
|
|
108
82
|
return explicit
|
|
109
83
|
end
|
|
110
84
|
|
|
111
|
-
|
|
112
|
-
wants_desktop = options[:desktop]
|
|
113
|
-
return "all" if wants_web && wants_desktop
|
|
114
|
-
return "web" if wants_web
|
|
115
|
-
return "desktop" if wants_desktop
|
|
85
|
+
return "desktop" if options[:desktop]
|
|
116
86
|
|
|
117
87
|
"none"
|
|
118
88
|
end
|
|
119
89
|
|
|
120
90
|
def desktop_requested?
|
|
121
|
-
|
|
91
|
+
requested_client == "desktop"
|
|
122
92
|
end
|
|
123
93
|
|
|
124
94
|
def install_target
|
|
@@ -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
|
|
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
|
|
37
|
-
say "
|
|
38
|
-
say "
|
|
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
|
|
44
|
-
|
|
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
|